mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
rename klo to ktx
This commit is contained in:
parent
1a42152e6f
commit
3ce510b55b
704 changed files with 10205 additions and 10255 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: KLO CI
|
||||
name: KTX CI
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -11,7 +11,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: klo-ci-${{ github.ref }}
|
||||
group: ktx-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: klo-package-artifacts-${{ github.sha }}
|
||||
name: ktx-package-artifacts-${{ github.sha }}
|
||||
path: |
|
||||
dist/artifacts/manifest.json
|
||||
dist/artifacts/npm/*.tgz
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -39,7 +39,7 @@ yarn-error.log*
|
|||
.pnpm-debug.log*
|
||||
|
||||
# Local project runtime state
|
||||
.klo/
|
||||
.ktx/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
|
|
|||
24
AGENTS.md
24
AGENTS.md
|
|
@ -22,7 +22,7 @@ database migrations, ORPC contracts, or `python-service/` layout exist here.
|
|||
- **MUST**: Remove dead code; do not leave commented-out code, unused wrappers,
|
||||
or empty directories.
|
||||
- **MUST**: Keep package/public API changes intentional. Do not add compatibility
|
||||
wrappers for old KLO names unless the user explicitly asks for a migration
|
||||
wrappers for old KTX names unless the user explicitly asks for a migration
|
||||
bridge.
|
||||
|
||||
### Absolute Prohibitions
|
||||
|
|
@ -65,14 +65,14 @@ KTX is a pnpm + uv workspace.
|
|||
- Core context package: `packages/context`
|
||||
- LLM package: `packages/llm`
|
||||
- Database connectors: `packages/connector-*`
|
||||
- Python semantic layer: `python/klo-sl`
|
||||
- Python daemon: `python/klo-daemon`
|
||||
- Python semantic layer: `python/ktx-sl`
|
||||
- Python daemon: `python/ktx-daemon`
|
||||
- Examples and fixtures: `examples/`
|
||||
- Workspace scripts: `scripts/`
|
||||
- Local agent skills are private overlays. Do not commit `.agents/` or
|
||||
`.claude/` to this public repository.
|
||||
|
||||
Some package names still contain `klo` during the split. Do not mass-rename
|
||||
Some package names still contain `ktx` during the split. Do not mass-rename
|
||||
symbols, package names, paths, or docs to `ktx` unless the task asks for that
|
||||
rename.
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ pnpm run build
|
|||
pnpm run type-check
|
||||
pnpm run test
|
||||
pnpm run check
|
||||
pnpm --filter @klo/cli run smoke
|
||||
pnpm --filter @ktx/cli run smoke
|
||||
pnpm --filter './packages/*' run build
|
||||
pnpm --filter './packages/*' run test
|
||||
pnpm --filter './packages/*' run type-check
|
||||
|
|
@ -97,8 +97,8 @@ pnpm --filter './packages/*' run type-check
|
|||
```bash
|
||||
uv sync --all-groups
|
||||
uv run pytest -q
|
||||
uv run pytest python/klo-sl/tests -q
|
||||
uv run pytest python/klo-daemon/tests -q
|
||||
uv run pytest python/ktx-sl/tests -q
|
||||
uv run pytest python/ktx-daemon/tests -q
|
||||
uv run pre-commit run --files [FILES]
|
||||
```
|
||||
|
||||
|
|
@ -127,8 +127,8 @@ shared contracts or package exports are affected.
|
|||
- Build/export changes: `pnpm run build`
|
||||
- Workspace scripts: `node --test scripts/*.test.mjs` or the specific script
|
||||
test file
|
||||
- Python semantic layer: `uv run pytest python/klo-sl/tests -q`
|
||||
- Python daemon: `uv run pytest python/klo-daemon/tests -q`
|
||||
- Python semantic layer: `uv run pytest python/ktx-sl/tests -q`
|
||||
- Python daemon: `uv run pytest python/ktx-daemon/tests -q`
|
||||
- Python files: also run `uv run pre-commit run --files [FILES]` when
|
||||
pre-commit is configured
|
||||
|
||||
|
|
@ -178,15 +178,15 @@ use `PascalCase` without the suffix.
|
|||
- Use `pathlib` instead of `os.path`.
|
||||
- Use `logger.exception()` when catching and logging exceptions.
|
||||
- Prefer explicit exception types over broad `except Exception`.
|
||||
- Keep `python/klo-sl` focused on semantic-layer planning and SQL generation.
|
||||
- Keep `python/klo-daemon` focused on portable daemon/API behavior around the
|
||||
- Keep `python/ktx-sl` focused on semantic-layer planning and SQL generation.
|
||||
- Keep `python/ktx-daemon` focused on portable daemon/API behavior around the
|
||||
semantic layer.
|
||||
|
||||
### SQL and Structured Parsing
|
||||
|
||||
- Prefer AST-based parsing over regex for structured input.
|
||||
- For SQL, use `sqlglot`; it is already a dependency.
|
||||
- In `python/klo-sl`, follow the local `python/klo-sl/AGENTS.md` guidance:
|
||||
- In `python/ktx-sl`, follow the local `python/ktx-sl/AGENTS.md` guidance:
|
||||
parse expressions with sqlglot, quote reserved identifiers before parsing,
|
||||
and generate postgres-shaped SQL before final dialect transpilation.
|
||||
- Regex may be used for non-structural sanitization, but not to interpret SQL
|
||||
|
|
|
|||
62
README.md
62
README.md
|
|
@ -1,14 +1,14 @@
|
|||
# KLO
|
||||
# KTX
|
||||
|
||||
KLO is a workspace-first context layer for database agents. It stores warehouse
|
||||
KTX is a workspace-first context layer for database agents. It stores warehouse
|
||||
memory in a project directory, generates and validates semantic-layer YAML,
|
||||
indexes knowledge, scans database schemas, and exposes the result through a CLI
|
||||
and MCP server.
|
||||
|
||||
KLO projects are plain files: YAML, Markdown, SQLite state, and generated
|
||||
KTX projects are plain files: YAML, Markdown, SQLite state, and generated
|
||||
artifacts. You can inspect them, commit them, and serve them to any MCP client.
|
||||
|
||||
## What KLO provides
|
||||
## What KTX provides
|
||||
|
||||
- Durable warehouse memory with semantic-layer sources and knowledge pages.
|
||||
- Native scan connectors for SQLite, Postgres, MySQL, ClickHouse, SQL Server,
|
||||
|
|
@ -25,8 +25,8 @@ Run the pre-seeded demo from the repository root:
|
|||
```bash
|
||||
pnpm install
|
||||
pnpm run setup:dev
|
||||
pnpm run klo -- setup demo --no-input
|
||||
pnpm run klo -- setup demo inspect
|
||||
pnpm run ktx -- setup demo --no-input
|
||||
pnpm run ktx -- setup demo inspect
|
||||
```
|
||||
|
||||
The default demo uses packaged sample data and prebuilt context. It does not
|
||||
|
|
@ -35,7 +35,7 @@ require API keys, network access, or an LLM provider.
|
|||
To replay the packaged ingest run, use:
|
||||
|
||||
```bash
|
||||
pnpm run klo -- setup demo --mode replay --no-input
|
||||
pnpm run ktx -- setup demo --mode replay --no-input
|
||||
```
|
||||
|
||||
To run the full agentic demo with an LLM provider, set a provider key for the
|
||||
|
|
@ -43,11 +43,11 @@ current process:
|
|||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=$YOUR_ANTHROPIC_API_KEY \
|
||||
pnpm run klo -- setup demo --mode full --no-input
|
||||
pnpm run ktx -- setup demo --mode full --no-input
|
||||
```
|
||||
|
||||
Interactive full-demo setup can prompt for a provider key without writing the
|
||||
key to `klo.yaml`.
|
||||
key to `ktx.yaml`.
|
||||
|
||||
## Build a local project
|
||||
|
||||
|
|
@ -57,8 +57,8 @@ Create a project from the repository root:
|
|||
uv sync --all-packages
|
||||
source .venv/bin/activate
|
||||
|
||||
PROJECT_DIR="$(mktemp -d)/klo-demo"
|
||||
pnpm run klo -- init "$PROJECT_DIR" --name klo-demo
|
||||
PROJECT_DIR="$(mktemp -d)/ktx-demo"
|
||||
pnpm run ktx -- init "$PROJECT_DIR" --name ktx-demo
|
||||
```
|
||||
|
||||
Create a SQLite warehouse:
|
||||
|
|
@ -88,11 +88,11 @@ conn.close()
|
|||
PY
|
||||
```
|
||||
|
||||
Replace the generated `klo.yaml`:
|
||||
Replace the generated `ktx.yaml`:
|
||||
|
||||
```bash
|
||||
cat > "$PROJECT_DIR/klo.yaml" <<YAML
|
||||
project: klo-demo
|
||||
cat > "$PROJECT_DIR/ktx.yaml" <<YAML
|
||||
project: ktx-demo
|
||||
connections:
|
||||
warehouse:
|
||||
driver: sqlite
|
||||
|
|
@ -103,7 +103,7 @@ storage:
|
|||
search: sqlite-fts5
|
||||
git:
|
||||
auto_commit: true
|
||||
author: "klo <klo@example.com>"
|
||||
author: "ktx <ktx@example.com>"
|
||||
memory:
|
||||
auto_commit: true
|
||||
YAML
|
||||
|
|
@ -112,7 +112,7 @@ YAML
|
|||
Write and validate a semantic-layer source:
|
||||
|
||||
```bash
|
||||
pnpm run klo -- sl write accounts --project-dir "$PROJECT_DIR" \
|
||||
pnpm run ktx -- sl write accounts --project-dir "$PROJECT_DIR" \
|
||||
--connection-id warehouse --yaml 'name: accounts
|
||||
table: accounts
|
||||
description: CRM accounts with segmentation attributes.
|
||||
|
|
@ -133,14 +133,14 @@ measures:
|
|||
joins: []
|
||||
'
|
||||
|
||||
pnpm run klo -- sl validate accounts --project-dir "$PROJECT_DIR" \
|
||||
pnpm run ktx -- sl validate accounts --project-dir "$PROJECT_DIR" \
|
||||
--connection-id warehouse
|
||||
```
|
||||
|
||||
Generate SQL and execute the query:
|
||||
|
||||
```bash
|
||||
pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
|
||||
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
|
||||
--connection-id warehouse \
|
||||
--measure accounts.account_count \
|
||||
--dimension accounts.segment \
|
||||
|
|
@ -148,7 +148,7 @@ pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
|
|||
--limit 5 \
|
||||
--format sql
|
||||
|
||||
pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
|
||||
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
|
||||
--connection-id warehouse \
|
||||
--measure accounts.account_count \
|
||||
--dimension accounts.segment \
|
||||
|
|
@ -161,8 +161,8 @@ pnpm run klo -- sl query --project-dir "$PROJECT_DIR" \
|
|||
List and test the warehouse connection:
|
||||
|
||||
```bash
|
||||
pnpm run klo -- connection list --project-dir "$PROJECT_DIR"
|
||||
pnpm run klo -- connection test warehouse --project-dir "$PROJECT_DIR"
|
||||
pnpm run ktx -- connection list --project-dir "$PROJECT_DIR"
|
||||
pnpm run ktx -- connection test warehouse --project-dir "$PROJECT_DIR"
|
||||
```
|
||||
|
||||
The connection test prints the configured driver and discovered table count:
|
||||
|
|
@ -179,11 +179,11 @@ Scan artifacts are written under
|
|||
|
||||
```bash
|
||||
|
||||
SCAN_OUTPUT="$(pnpm run klo -- scan warehouse --project-dir "$PROJECT_DIR")"
|
||||
SCAN_OUTPUT="$(pnpm run ktx -- scan warehouse --project-dir "$PROJECT_DIR")"
|
||||
printf '%s\n' "$SCAN_OUTPUT"
|
||||
SCAN_RUN_ID="$(printf '%s\n' "$SCAN_OUTPUT" | awk '/^Run: / { print $2 }')"
|
||||
pnpm run klo -- scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
|
||||
pnpm run klo -- scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
|
||||
pnpm run ktx -- scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
|
||||
pnpm run ktx -- scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
|
||||
```
|
||||
|
||||
For non-SQLite drivers, prefer credential references such as `--url env:NAME`
|
||||
|
|
@ -195,13 +195,13 @@ Start the Python compute daemon in one terminal:
|
|||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
uv run klo-daemon serve-http --host 127.0.0.1 --port 8765
|
||||
uv run ktx-daemon serve-http --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
Start the stdio MCP server in another terminal:
|
||||
|
||||
```bash
|
||||
pnpm run klo -- serve --mcp stdio --project-dir "$PROJECT_DIR" \
|
||||
pnpm run ktx -- serve --mcp stdio --project-dir "$PROJECT_DIR" \
|
||||
--user-id local \
|
||||
--semantic-compute-url http://127.0.0.1:8765 \
|
||||
--execute-queries
|
||||
|
|
@ -225,8 +225,8 @@ The MCP server exposes `connection_list`, `knowledge_search`,
|
|||
- `packages/connector-snowflake`: Snowflake scan connector.
|
||||
- `packages/connector-sqlite`: SQLite scan connector.
|
||||
- `packages/connector-sqlserver`: SQL Server scan connector.
|
||||
- `python/klo-sl`: semantic-layer engine.
|
||||
- `python/klo-daemon`: portable compute service for semantic-layer operations.
|
||||
- `python/ktx-sl`: semantic-layer engine.
|
||||
- `python/ktx-daemon`: portable compute service for semantic-layer operations.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -240,11 +240,11 @@ source .venv/bin/activate
|
|||
uv run pytest
|
||||
```
|
||||
|
||||
Use the optional development binary when you want a local `klo-dev` command:
|
||||
Use the optional development binary when you want a local `ktx-dev` command:
|
||||
|
||||
```bash
|
||||
pnpm run link:dev
|
||||
klo-dev --help
|
||||
ktx-dev --help
|
||||
```
|
||||
|
||||
The repository uses `pnpm` for TypeScript packages and `uv` for Python
|
||||
|
|
@ -267,4 +267,4 @@ pnpm run release:readiness
|
|||
|
||||
## License
|
||||
|
||||
KLO is licensed under the Apache License, Version 2.0. See `LICENSE`.
|
||||
KTX is licensed under the Apache License, Version 2.0. See `LICENSE`.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
# klo examples
|
||||
# ktx examples
|
||||
|
||||
## local-warehouse
|
||||
|
||||
`local-warehouse/` is a runnable standalone KLO project for local CLI and MCP
|
||||
`local-warehouse/` is a runnable standalone KTX project for local CLI and MCP
|
||||
smoke testing. It uses the fake ingest adapter and does not require a database
|
||||
or external app server.
|
||||
|
||||
Copy it before running commands:
|
||||
|
||||
```bash
|
||||
pnpm --filter @klo/cli run build
|
||||
pnpm --filter @ktx/cli run build
|
||||
EXAMPLE_DIR="$(mktemp -d)/local-warehouse"
|
||||
cp -R examples/local-warehouse "$EXAMPLE_DIR"
|
||||
node packages/cli/dist/bin.js knowledge list --project-dir "$EXAMPLE_DIR"
|
||||
|
|
@ -21,7 +21,7 @@ The copied project initializes its own Git repository on first use.
|
|||
|
||||
## orbit-relationship-verification
|
||||
|
||||
`orbit-relationship-verification/` is a checked-in KLO project used by
|
||||
`orbit-relationship-verification/` is a checked-in KTX project used by
|
||||
`pnpm run relationships:verify-orbit`. It points the `orbit` SQLite connection
|
||||
at the Orbit-style no-declared-constraint relationship fixture and verifies that
|
||||
relationship enrichment writes nine accepted joins without requiring a local
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# Local Warehouse Example
|
||||
|
||||
This example is a standalone KLO project that can be copied to a temp directory
|
||||
This example is a standalone KTX project that can be copied to a temp directory
|
||||
and used with the local CLI and stdio MCP server. It uses the `fake` ingest
|
||||
adapter so it does not require a database or external app server.
|
||||
|
||||
Run the example from the repository root after building the CLI:
|
||||
|
||||
```bash
|
||||
pnpm --filter @klo/cli run build
|
||||
pnpm --filter @ktx/cli run build
|
||||
EXAMPLE_DIR="$(mktemp -d)/local-warehouse"
|
||||
cp -R examples/local-warehouse "$EXAMPLE_DIR"
|
||||
node packages/cli/dist/bin.js knowledge list --project-dir "$EXAMPLE_DIR"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ storage:
|
|||
search: sqlite-fts5
|
||||
git:
|
||||
auto_commit: true
|
||||
author: "klo <klo@example.com>"
|
||||
author: "ktx <ktx@example.com>"
|
||||
ingest:
|
||||
adapters:
|
||||
- fake
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# Orbit-style relationship discovery verification
|
||||
|
||||
This KLO project backs the default `relationships:verify-orbit` command. It uses
|
||||
This KTX project backs the default `relationships:verify-orbit` command. It uses
|
||||
the checked-in Orbit-style SQLite fixture from the relationship discovery
|
||||
benchmark corpus, with no declared primary keys or foreign keys in the database
|
||||
schema.
|
||||
|
||||
Run from the KLO workspace root:
|
||||
Run from the KTX workspace root:
|
||||
|
||||
```bash
|
||||
pnpm run relationships:verify-orbit
|
||||
|
|
@ -29,5 +29,5 @@ examples/orbit-relationship-verification/reports/orbit-verification.md
|
|||
Use a real local Orbit project by overriding the project directory:
|
||||
|
||||
```bash
|
||||
KLO_ORBIT_PROJECT_DIR=/path/to/orbit-project pnpm run relationships:verify-orbit
|
||||
KTX_ORBIT_PROJECT_DIR=/path/to/orbit-project pnpm run relationships:verify-orbit
|
||||
```
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ storage:
|
|||
search: sqlite-fts5
|
||||
git:
|
||||
auto_commit: true
|
||||
author: "klo <klo@example.com>"
|
||||
author: "ktx <ktx@example.com>"
|
||||
ingest:
|
||||
adapters:
|
||||
- live-database
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
# Package artifact smoke checks
|
||||
|
||||
The package artifact smoke checks create temporary projects instead of storing
|
||||
sample projects in this directory. Run the checks from `klo/`:
|
||||
sample projects in this directory. Run the checks from `ktx/`:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pnpm run artifacts:check
|
||||
```
|
||||
|
||||
The npm smoke project installs the generated `@klo/context` and `@klo/cli`
|
||||
tarballs, imports public package entry points, and runs installed `klo`
|
||||
The npm smoke project installs the generated `@ktx/context` and `@ktx/cli`
|
||||
tarballs, imports public package entry points, and runs installed `ktx`
|
||||
commands against a generated local project.
|
||||
|
||||
The Python smoke project installs `klo-daemon` through the local artifact
|
||||
directory, imports `semantic_layer` and `klo_daemon`, and runs
|
||||
`python -m klo_daemon semantic-validate`.
|
||||
The Python smoke project installs `ktx-daemon` through the local artifact
|
||||
directory, imports `semantic_layer` and `ktx_daemon`, and runs
|
||||
`python -m ktx_daemon semantic-validate`.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This example is a manual smoke for Postgres historic-SQL ingest through
|
||||
`pg_stat_statements`. It starts Postgres 14 with the extension preloaded,
|
||||
generates query workload under separate users, runs `klo setup` with
|
||||
generates query workload under separate users, runs `ktx setup` with
|
||||
`--enable-historic-sql`, and verifies three local ingest runs:
|
||||
|
||||
- first run creates a fresh PGSS baseline
|
||||
|
|
@ -12,30 +12,30 @@ generates query workload under separate users, runs `klo setup` with
|
|||
## Prerequisites
|
||||
|
||||
- Docker with Compose v2
|
||||
- Node and pnpm matching the KLO workspace
|
||||
- `python-service/.venv` already created, or `KLO_SQL_ANALYSIS_URL` pointing at
|
||||
- Node and pnpm matching the KTX workspace
|
||||
- `python-service/.venv` already created, or `KTX_SQL_ANALYSIS_URL` pointing at
|
||||
a running service that exposes `/api/sql/analyze-for-fingerprint`
|
||||
|
||||
## Run
|
||||
|
||||
From the KLO repository root:
|
||||
From the KTX repository root:
|
||||
|
||||
```bash
|
||||
examples/postgres-historic/scripts/smoke.sh
|
||||
```
|
||||
|
||||
The smoke creates a temporary KLO project, starts Postgres on
|
||||
The smoke creates a temporary KTX project, starts Postgres on
|
||||
`127.0.0.1:55432`, and uses this connection URL:
|
||||
|
||||
```bash
|
||||
postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
|
||||
postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
|
||||
```
|
||||
|
||||
Set `KLO_POSTGRES_HISTORIC_KEEP_DOCKER=1` to leave the container running after
|
||||
Set `KTX_POSTGRES_HISTORIC_KEEP_DOCKER=1` to leave the container running after
|
||||
the script exits.
|
||||
|
||||
The smoke validates the historic-SQL raw snapshot path without requiring LLM
|
||||
credentials. It uses KLO's local stage-only ingest API after `klo setup` so the
|
||||
credentials. It uses KTX's local stage-only ingest API after `ktx setup` so the
|
||||
PGSS baseline and delta behavior can be checked independently from curation.
|
||||
|
||||
## Manual Commands
|
||||
|
|
@ -50,9 +50,9 @@ examples/postgres-historic/scripts/generate-workload.sh base
|
|||
Create a project and enable historic SQL:
|
||||
|
||||
```bash
|
||||
export WAREHOUSE_DATABASE_URL=postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
|
||||
pnpm --filter @klo/cli run build
|
||||
node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic setup \
|
||||
export WAREHOUSE_DATABASE_URL=postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics # pragma: allowlist secret
|
||||
pnpm --filter @ktx/cli run build
|
||||
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic setup \
|
||||
--new \
|
||||
--skip-agents \
|
||||
--skip-llm \
|
||||
|
|
@ -71,11 +71,11 @@ node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic setup \
|
|||
### Readiness check
|
||||
|
||||
```bash
|
||||
pnpm run klo -- dev doctor --project-dir /tmp/klo-postgres-historic --no-input
|
||||
pnpm run ktx -- dev doctor --project-dir /tmp/ktx-postgres-historic --no-input
|
||||
```
|
||||
|
||||
The installed CLI form is `klo dev doctor --project-dir
|
||||
/tmp/klo-postgres-historic --no-input`. Expected output includes `PASS Postgres
|
||||
The installed CLI form is `ktx dev doctor --project-dir
|
||||
/tmp/ktx-postgres-historic --no-input`. Expected output includes `PASS Postgres
|
||||
Historic SQL (warehouse)` when `pg_stat_statements` is installed,
|
||||
`pg_read_all_stats` is granted, tracking is enabled, and
|
||||
`pg_stat_statements.max` is at least 5000.
|
||||
|
|
@ -83,7 +83,7 @@ Historic SQL (warehouse)` when `pg_stat_statements` is installed,
|
|||
Run local historic-SQL ingest:
|
||||
|
||||
```bash
|
||||
node packages/cli/dist/bin.js --project-dir /tmp/klo-postgres-historic dev ingest run \
|
||||
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic dev ingest run \
|
||||
--connection-id warehouse \
|
||||
--adapter historic-sql \
|
||||
--plain \
|
||||
|
|
@ -96,7 +96,7 @@ configured LLM provider.
|
|||
Inspect the latest manifest:
|
||||
|
||||
```bash
|
||||
find /tmp/klo-postgres-historic/raw-sources/warehouse/historic-sql -name manifest.json | sort | tail -n 1
|
||||
find /tmp/ktx-postgres-historic/raw-sources/warehouse/historic-sql -name manifest.json | sort | tail -n 1
|
||||
```
|
||||
|
||||
The manifest should have `dialect: "postgres"`, `degraded: true`,
|
||||
|
|
@ -108,8 +108,8 @@ The manifest should have `dialect: "postgres"`, `degraded: true`,
|
|||
- Missing extension: confirm `shared_preload_libraries=pg_stat_statements` and
|
||||
`CREATE EXTENSION pg_stat_statements;` both happened in the `analytics`
|
||||
database.
|
||||
- Missing grants: confirm `GRANT pg_read_all_stats TO klo_reader;`.
|
||||
- Missing grants: confirm `GRANT pg_read_all_stats TO ktx_reader;`.
|
||||
- Empty templates: rerun `scripts/generate-workload.sh base` and keep
|
||||
`--historic-sql-min-calls 2` for the smoke.
|
||||
- SQL-analysis failures: set `KLO_SQL_ANALYSIS_URL` to the running service URL
|
||||
- SQL-analysis failures: set `KTX_SQL_ANALYSIS_URL` to the running service URL
|
||||
or create `python-service/.venv` before running `scripts/smoke.sh`.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
|||
|
||||
CREATE ROLE app_user LOGIN PASSWORD 'app_pass';
|
||||
CREATE ROLE etl_user LOGIN PASSWORD 'etl_pass';
|
||||
CREATE ROLE klo_reader LOGIN PASSWORD 'klo_reader';
|
||||
CREATE ROLE ktx_reader LOGIN PASSWORD 'ktx_reader';
|
||||
|
||||
GRANT pg_read_all_stats TO klo_reader;
|
||||
GRANT pg_read_all_stats TO ktx_reader;
|
||||
|
||||
CREATE TABLE customers (
|
||||
id integer PRIMARY KEY,
|
||||
|
|
@ -47,5 +47,5 @@ INSERT INTO events (id, customer_id, event_name, occurred_at) VALUES
|
|||
(4, 3, 'sync_completed', now() - interval '6 hours'),
|
||||
(5, 4, 'dashboard_viewed', now() - interval '5 hours');
|
||||
|
||||
GRANT USAGE ON SCHEMA public TO app_user, etl_user, klo_reader;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user, etl_user, klo_reader;
|
||||
GRANT USAGE ON SCHEMA public TO app_user, etl_user, ktx_reader;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user, etl_user, ktx_reader;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ set -euo pipefail
|
|||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
EXAMPLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
KLO_ROOT="$(cd "$EXAMPLE_DIR/../.." && pwd)"
|
||||
REPO_ROOT="$(cd "$KLO_ROOT/.." && pwd)"
|
||||
KTX_ROOT="$(cd "$EXAMPLE_DIR/../.." && pwd)"
|
||||
REPO_ROOT="$(cd "$KTX_ROOT/.." && pwd)"
|
||||
COMPOSE_FILE="$EXAMPLE_DIR/docker-compose.yml"
|
||||
PROJECT_PARENT="${KLO_POSTGRES_HISTORIC_PROJECT_PARENT:-$(mktemp -d)}"
|
||||
PROJECT_DIR="$PROJECT_PARENT/postgres-historic-klo"
|
||||
KLO_BIN="$KLO_ROOT/packages/cli/dist/bin.js"
|
||||
PROJECT_PARENT="${KTX_POSTGRES_HISTORIC_PROJECT_PARENT:-$(mktemp -d)}"
|
||||
PROJECT_DIR="$PROJECT_PARENT/postgres-historic-ktx"
|
||||
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
|
||||
PYTHON_SERVICE_LOG="$PROJECT_PARENT/python-service.log"
|
||||
PYTHON_SERVICE_PID=""
|
||||
|
||||
|
|
@ -16,18 +16,18 @@ cleanup() {
|
|||
if [[ -n "$PYTHON_SERVICE_PID" ]]; then
|
||||
kill "$PYTHON_SERVICE_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ "${KLO_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
|
||||
if [[ "${KTX_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
|
||||
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
start_sql_analysis_if_needed() {
|
||||
if [[ -n "${KLO_SQL_ANALYSIS_URL:-}" ]]; then
|
||||
if [[ -n "${KTX_SQL_ANALYSIS_URL:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -d "$REPO_ROOT/python-service/.venv" ]]; then
|
||||
echo "Set KLO_SQL_ANALYSIS_URL or create python-service/.venv before running this smoke." >&2
|
||||
echo "Set KTX_SQL_ANALYSIS_URL or create python-service/.venv before running this smoke." >&2
|
||||
exit 1
|
||||
fi
|
||||
(
|
||||
|
|
@ -36,9 +36,9 @@ start_sql_analysis_if_needed() {
|
|||
uvicorn app.main:app --host 127.0.0.1 --port 18081 >"$PYTHON_SERVICE_LOG" 2>&1
|
||||
) &
|
||||
PYTHON_SERVICE_PID="$!"
|
||||
export KLO_SQL_ANALYSIS_URL="http://127.0.0.1:18081"
|
||||
export KTX_SQL_ANALYSIS_URL="http://127.0.0.1:18081"
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS "$KLO_SQL_ANALYSIS_URL/health" >/dev/null 2>&1; then
|
||||
if curl -fsS "$KTX_SQL_ANALYSIS_URL/health" >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
|
|
@ -74,18 +74,18 @@ NODE
|
|||
|
||||
run_historic_stage_only() {
|
||||
local job_id="$1"
|
||||
node - "$KLO_ROOT" "$PROJECT_DIR" "$job_id" <<'NODE'
|
||||
node - "$KTX_ROOT" "$PROJECT_DIR" "$job_id" <<'NODE'
|
||||
const { join } = await import('node:path');
|
||||
|
||||
const kloRoot = process.argv[2];
|
||||
const ktxRoot = process.argv[2];
|
||||
const projectDir = process.argv[3];
|
||||
const jobId = process.argv[4];
|
||||
const { loadKloProject } = await import(join(kloRoot, 'packages/context/dist/project/index.js'));
|
||||
const { runLocalStageOnlyIngest } = await import(join(kloRoot, 'packages/context/dist/ingest/index.js'));
|
||||
const { createKloCliLocalIngestAdapters } = await import(join(kloRoot, 'packages/cli/dist/local-adapters.js'));
|
||||
const { loadKtxProject } = await import(join(ktxRoot, 'packages/context/dist/project/index.js'));
|
||||
const { runLocalStageOnlyIngest } = await import(join(ktxRoot, 'packages/context/dist/ingest/index.js'));
|
||||
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
|
||||
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const adapters = createKloCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const adapters = createKtxCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
|
||||
const adapter = adapters.find((candidate) => candidate.source === 'historic-sql');
|
||||
if (!adapter) throw new Error('historic-sql adapter was not registered for local run');
|
||||
const record = await runLocalStageOnlyIngest({
|
||||
|
|
@ -102,22 +102,22 @@ await adapter.onPullSucceeded?.({
|
|||
syncId: record.syncId,
|
||||
trigger: 'manual_resync',
|
||||
completedAt: new Date(record.completedAt),
|
||||
stagedDir: join(project.projectDir, '.klo/cache/local-ingest', jobId, 'staged'),
|
||||
stagedDir: join(project.projectDir, '.ktx/cache/local-ingest', jobId, 'staged'),
|
||||
});
|
||||
console.log(record.syncId);
|
||||
NODE
|
||||
}
|
||||
|
||||
cd "$KLO_ROOT"
|
||||
pnpm --filter @klo/context run build
|
||||
pnpm --filter @klo/cli run build
|
||||
cd "$KTX_ROOT"
|
||||
pnpm --filter @ktx/context run build
|
||||
pnpm --filter @ktx/cli run build
|
||||
start_sql_analysis_if_needed
|
||||
|
||||
docker compose -f "$COMPOSE_FILE" up -d --wait
|
||||
"$EXAMPLE_DIR/scripts/generate-workload.sh" base
|
||||
|
||||
export WAREHOUSE_DATABASE_URL="${WAREHOUSE_DATABASE_URL:-postgresql://klo_reader:klo_reader@127.0.0.1:55432/analytics}" # pragma: allowlist secret
|
||||
node "$KLO_BIN" --project-dir "$PROJECT_DIR" setup \
|
||||
export WAREHOUSE_DATABASE_URL="${WAREHOUSE_DATABASE_URL:-postgresql://ktx_reader:ktx_reader@127.0.0.1:55432/analytics}" # pragma: allowlist secret
|
||||
node "$KTX_BIN" --project-dir "$PROJECT_DIR" setup \
|
||||
--new \
|
||||
--skip-agents \
|
||||
--skip-llm \
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "klo-workspace",
|
||||
"name": "ktx-workspace",
|
||||
"version": "0.0.0-private",
|
||||
"description": "Workspace root for klo packages",
|
||||
"description": "Workspace root for ktx packages",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"artifacts:verify-manifest": "node scripts/package-artifacts.mjs verify-manifest",
|
||||
"build": "pnpm --filter './packages/*' run build",
|
||||
"check": "node scripts/check-boundaries.mjs && node --test scripts/*.test.mjs && pnpm --filter './packages/*' run build && pnpm --filter './packages/*' run test",
|
||||
"klo": "node scripts/run-klo.mjs",
|
||||
"ktx": "node scripts/run-ktx.mjs",
|
||||
"link:dev": "node scripts/link-dev-cli.mjs",
|
||||
"native:rebuild": "pnpm -r rebuild better-sqlite3",
|
||||
"setup:dev": "node scripts/setup-dev.mjs",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"relationships:rebuild-public-snapshots": "node scripts/build-benchmark-snapshot.mjs --rebuild-all",
|
||||
"relationships:build-adventureworks-oltp": "node scripts/build-adventureworks-oltp-fixture.mjs",
|
||||
"relationships:verify-orbit": "node scripts/relationship-orbit-verification.mjs",
|
||||
"smoke": "pnpm run build && pnpm --filter @klo/cli run smoke",
|
||||
"smoke": "pnpm run build && pnpm --filter @ktx/cli run smoke",
|
||||
"test": "node --test scripts/*.test.mjs && pnpm --filter './packages/*' run test",
|
||||
"type-check": "pnpm --filter './packages/*' run type-check"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "@klo/cli",
|
||||
"name": "@ktx/cli",
|
||||
"version": "0.0.0-private",
|
||||
"description": "CLI wrapper for klo context packages",
|
||||
"description": "CLI wrapper for ktx context packages",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"klo": "./dist/bin.js"
|
||||
"ktx": "./dist/bin.js"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
@ -34,16 +34,16 @@
|
|||
"dependencies": {
|
||||
"@clack/prompts": "1.3.0",
|
||||
"@commander-js/extra-typings": "14.0.0",
|
||||
"@klo/connector-bigquery": "workspace:*",
|
||||
"@klo/connector-clickhouse": "workspace:*",
|
||||
"@klo/connector-mysql": "workspace:*",
|
||||
"@klo/connector-postgres": "workspace:*",
|
||||
"@klo/connector-posthog": "workspace:*",
|
||||
"@klo/connector-snowflake": "workspace:*",
|
||||
"@klo/connector-sqlite": "workspace:*",
|
||||
"@klo/connector-sqlserver": "workspace:*",
|
||||
"@klo/context": "workspace:*",
|
||||
"@klo/llm": "workspace:*",
|
||||
"@ktx/connector-bigquery": "workspace:*",
|
||||
"@ktx/connector-clickhouse": "workspace:*",
|
||||
"@ktx/connector-mysql": "workspace:*",
|
||||
"@ktx/connector-postgres": "workspace:*",
|
||||
"@ktx/connector-posthog": "workspace:*",
|
||||
"@ktx/connector-snowflake": "workspace:*",
|
||||
"@ktx/connector-sqlite": "workspace:*",
|
||||
"@ktx/connector-sqlserver": "workspace:*",
|
||||
"@ktx/context": "workspace:*",
|
||||
"@ktx/llm": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"commander": "14.0.3",
|
||||
"ink": "^7.0.1",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Database from 'better-sqlite3';
|
|||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const repoRoot = resolve(packageRoot, '../..');
|
||||
const defaultDemoSource = resolve(repoRoot, '../../../orbit-demo-source');
|
||||
const sourceRoot = resolve(process.env.KLO_DEMO_SOURCE_DIR ?? defaultDemoSource);
|
||||
const sourceRoot = resolve(process.env.KTX_DEMO_SOURCE_DIR ?? defaultDemoSource);
|
||||
const assetDir = join(packageRoot, 'assets/demo/orbit');
|
||||
const dbPath = join(assetDir, 'demo.db');
|
||||
const exampleDbtProjectDir = ['dbt', `${'kae'}lio_demo`].join('/');
|
||||
|
|
@ -330,7 +330,7 @@ async function pathExists(path) {
|
|||
async function assertReadable(path, label) {
|
||||
if (!(await pathExists(path))) {
|
||||
throw new Error(
|
||||
`${label} not found at ${path}. Set KLO_DEMO_SOURCE_DIR to the Orbit demo source directory.`,
|
||||
`${label} not found at ${path}. Set KTX_DEMO_SOURCE_DIR to the Orbit demo source directory.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { tmpdir } from 'node:os';
|
|||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
KLO_AGENT_MAX_ROWS_CAP,
|
||||
createKloAgentRuntime,
|
||||
KTX_AGENT_MAX_ROWS_CAP,
|
||||
createKtxAgentRuntime,
|
||||
parseAgentMaxRows,
|
||||
readAgentJsonFile,
|
||||
writeAgentJson,
|
||||
|
|
@ -28,7 +28,7 @@ describe('agent runtime helpers', () => {
|
|||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-runtime-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-runtime-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -69,13 +69,13 @@ describe('agent runtime helpers', () => {
|
|||
expect(parseAgentMaxRows(100)).toBe(100);
|
||||
expect(() => parseAgentMaxRows(undefined)).toThrow('maxRows is required');
|
||||
expect(() => parseAgentMaxRows(0)).toThrow('positive integer');
|
||||
expect(() => parseAgentMaxRows(KLO_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KLO_AGENT_MAX_ROWS_CAP));
|
||||
expect(() => parseAgentMaxRows(KTX_AGENT_MAX_ROWS_CAP + 1)).toThrow(String(KTX_AGENT_MAX_ROWS_CAP));
|
||||
});
|
||||
|
||||
it('constructs local context ports with semantic compute and query executor', async () => {
|
||||
const project = {
|
||||
projectDir: tempDir,
|
||||
configPath: join(tempDir, 'klo.yaml'),
|
||||
configPath: join(tempDir, 'ktx.yaml'),
|
||||
config: { project: 'revenue', connections: {} },
|
||||
coreConfig: {},
|
||||
git: {},
|
||||
|
|
@ -88,7 +88,7 @@ describe('agent runtime helpers', () => {
|
|||
const createContextTools = vi.fn(() => ports);
|
||||
|
||||
await expect(
|
||||
createKloAgentRuntime(
|
||||
createKtxAgentRuntime(
|
||||
{ projectDir: tempDir, enableSemanticCompute: true, enableQueryExecution: true },
|
||||
{
|
||||
loadProject,
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { createDefaultLocalQueryExecutor, type KloSqlQueryExecutorPort } from '@klo/context/connections';
|
||||
import { createPythonSemanticLayerComputePort, type KloSemanticLayerComputePort } from '@klo/context/daemon';
|
||||
import { createLocalProjectMcpContextPorts, type KloMcpContextPorts } from '@klo/context/mcp';
|
||||
import { type KloLocalProject, loadKloProject } from '@klo/context/project';
|
||||
import type { KloCliIo } from './cli-runtime.js';
|
||||
import { createDefaultLocalQueryExecutor, type KtxSqlQueryExecutorPort } from '@ktx/context/connections';
|
||||
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
||||
import { createLocalProjectMcpContextPorts, type KtxMcpContextPorts } from '@ktx/context/mcp';
|
||||
import { type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
|
||||
export const KLO_AGENT_MAX_ROWS_CAP = 1000;
|
||||
export const KTX_AGENT_MAX_ROWS_CAP = 1000;
|
||||
|
||||
export interface KloAgentRuntimeOptions {
|
||||
export interface KtxAgentRuntimeOptions {
|
||||
projectDir: string;
|
||||
enableSemanticCompute: boolean;
|
||||
enableQueryExecution: boolean;
|
||||
}
|
||||
|
||||
export interface KloAgentRuntime {
|
||||
project: KloLocalProject;
|
||||
ports: KloMcpContextPorts;
|
||||
semanticLayerCompute?: KloSemanticLayerComputePort;
|
||||
queryExecutor?: KloSqlQueryExecutorPort;
|
||||
export interface KtxAgentRuntime {
|
||||
project: KtxLocalProject;
|
||||
ports: KtxMcpContextPorts;
|
||||
semanticLayerCompute?: KtxSemanticLayerComputePort;
|
||||
queryExecutor?: KtxSqlQueryExecutorPort;
|
||||
}
|
||||
|
||||
export interface KloAgentRuntimeDeps {
|
||||
loadProject?: typeof loadKloProject;
|
||||
export interface KtxAgentRuntimeDeps {
|
||||
loadProject?: typeof loadKtxProject;
|
||||
createContextTools?: typeof createLocalProjectMcpContextPorts;
|
||||
createSemanticLayerCompute?: () => KloSemanticLayerComputePort;
|
||||
createQueryExecutor?: () => KloSqlQueryExecutorPort;
|
||||
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
|
||||
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
|
||||
}
|
||||
|
||||
export function writeAgentJson(io: KloCliIo, value: unknown): void {
|
||||
export function writeAgentJson(io: KtxCliIo, value: unknown): void {
|
||||
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
export function writeAgentJsonError(
|
||||
io: KloCliIo,
|
||||
io: KtxCliIo,
|
||||
message: string,
|
||||
detail: Record<string, unknown> = {},
|
||||
): void {
|
||||
|
|
@ -51,17 +51,17 @@ export function parseAgentMaxRows(value: number | undefined): number {
|
|||
if (!Number.isInteger(value) || value === undefined || value <= 0) {
|
||||
throw new Error('maxRows is required and must be a positive integer.');
|
||||
}
|
||||
if (value > KLO_AGENT_MAX_ROWS_CAP) {
|
||||
throw new Error(`maxRows must be less than or equal to ${KLO_AGENT_MAX_ROWS_CAP}.`);
|
||||
if (value > KTX_AGENT_MAX_ROWS_CAP) {
|
||||
throw new Error(`maxRows must be less than or equal to ${KTX_AGENT_MAX_ROWS_CAP}.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function createKloAgentRuntime(
|
||||
options: KloAgentRuntimeOptions,
|
||||
deps: KloAgentRuntimeDeps = {},
|
||||
): Promise<KloAgentRuntime> {
|
||||
const project = await (deps.loadProject ?? loadKloProject)({ projectDir: options.projectDir });
|
||||
export async function createKtxAgentRuntime(
|
||||
options: KtxAgentRuntimeOptions,
|
||||
deps: KtxAgentRuntimeDeps = {},
|
||||
): Promise<KtxAgentRuntime> {
|
||||
const project = await (deps.loadProject ?? loadKtxProject)({ projectDir: options.projectDir });
|
||||
const semanticLayerCompute = options.enableSemanticCompute
|
||||
? (deps.createSemanticLayerCompute ?? createPythonSemanticLayerComputePort)()
|
||||
: undefined;
|
||||
|
|
|
|||
|
|
@ -9,40 +9,40 @@ import {
|
|||
|
||||
describe('agent semantic-layer search readiness guidance', () => {
|
||||
it('formats missing project guidance with exact recovery commands', () => {
|
||||
expect(missingProjectSlSearchReadiness('/tmp/klo-search', 'gross revenue')).toEqual({
|
||||
expect(missingProjectSlSearchReadiness('/tmp/ktx-search', 'gross revenue')).toEqual({
|
||||
code: 'agent_sl_search_missing_project',
|
||||
message: 'Semantic-layer search needs an initialized KLO project at /tmp/klo-search.',
|
||||
message: 'Semantic-layer search needs an initialized KTX project at /tmp/ktx-search.',
|
||||
nextSteps: [
|
||||
'klo demo',
|
||||
'klo setup --project-dir /tmp/klo-search',
|
||||
'klo ingest <connection>',
|
||||
'klo agent sl list --json --query "gross revenue" --project-dir /tmp/klo-search',
|
||||
'ktx demo',
|
||||
'ktx setup --project-dir /tmp/ktx-search',
|
||||
'ktx ingest <connection>',
|
||||
'ktx agent sl list --json --query "gross revenue" --project-dir /tmp/ktx-search',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('formats no-connection and no-index guidance without hiding the project path', () => {
|
||||
expect(noConnectionsSlSearchReadiness('/tmp/klo-search', 'revenue')).toMatchObject({
|
||||
expect(noConnectionsSlSearchReadiness('/tmp/ktx-search', 'revenue')).toMatchObject({
|
||||
code: 'agent_sl_search_no_connections',
|
||||
message: 'Semantic-layer search found no configured connections in /tmp/klo-search.',
|
||||
message: 'Semantic-layer search found no configured connections in /tmp/ktx-search.',
|
||||
});
|
||||
expect(noIndexedSourcesSlSearchReadiness('/tmp/klo-search', 'orders')).toMatchObject({
|
||||
expect(noIndexedSourcesSlSearchReadiness('/tmp/ktx-search', 'orders')).toMatchObject({
|
||||
code: 'agent_sl_search_no_indexed_sources',
|
||||
message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/klo-search.',
|
||||
message: 'Semantic-layer search found no indexed semantic-layer sources in /tmp/ktx-search.',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats unknown connection guidance', () => {
|
||||
expect(missingConnectionSlSearchReadiness('/tmp/klo-search', 'warehouse', 'revenue')).toMatchObject({
|
||||
expect(missingConnectionSlSearchReadiness('/tmp/ktx-search', 'warehouse', 'revenue')).toMatchObject({
|
||||
code: 'agent_sl_search_unknown_connection',
|
||||
message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/klo-search.',
|
||||
message: 'Semantic-layer search connection "warehouse" is not configured in /tmp/ktx-search.',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects missing klo.yaml read errors', () => {
|
||||
it('detects missing ktx.yaml read errors', () => {
|
||||
const error = Object.assign(new Error('ENOENT: no such file or directory'), {
|
||||
code: 'ENOENT',
|
||||
path: '/tmp/klo-search/klo.yaml',
|
||||
path: '/tmp/ktx-search/ktx.yaml',
|
||||
});
|
||||
|
||||
expect(isMissingProjectConfigError(error)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
export type KloAgentSlSearchReadinessCode =
|
||||
export type KtxAgentSlSearchReadinessCode =
|
||||
| 'agent_sl_search_missing_project'
|
||||
| 'agent_sl_search_no_connections'
|
||||
| 'agent_sl_search_unknown_connection'
|
||||
| 'agent_sl_search_no_indexed_sources';
|
||||
|
||||
export interface KloAgentSlSearchReadinessDetail {
|
||||
code: KloAgentSlSearchReadinessCode;
|
||||
export interface KtxAgentSlSearchReadinessDetail {
|
||||
code: KtxAgentSlSearchReadinessCode;
|
||||
message: string;
|
||||
nextSteps: string[];
|
||||
}
|
||||
|
|
@ -16,14 +16,14 @@ function queryForCommand(query: string | undefined): string {
|
|||
}
|
||||
|
||||
function projectSearchCommand(projectDir: string, query: string | undefined): string {
|
||||
return `klo agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
|
||||
return `ktx agent sl list --json --query ${JSON.stringify(queryForCommand(query))} --project-dir ${projectDir}`;
|
||||
}
|
||||
|
||||
function baseNextSteps(projectDir: string, query: string | undefined): string[] {
|
||||
return [
|
||||
'klo demo',
|
||||
`klo setup --project-dir ${projectDir}`,
|
||||
'klo ingest <connection>',
|
||||
'ktx demo',
|
||||
`ktx setup --project-dir ${projectDir}`,
|
||||
'ktx ingest <connection>',
|
||||
projectSearchCommand(projectDir, query),
|
||||
];
|
||||
}
|
||||
|
|
@ -31,10 +31,10 @@ function baseNextSteps(projectDir: string, query: string | undefined): string[]
|
|||
export function missingProjectSlSearchReadiness(
|
||||
projectDir: string,
|
||||
query: string | undefined,
|
||||
): KloAgentSlSearchReadinessDetail {
|
||||
): KtxAgentSlSearchReadinessDetail {
|
||||
return {
|
||||
code: 'agent_sl_search_missing_project',
|
||||
message: `Semantic-layer search needs an initialized KLO project at ${projectDir}.`,
|
||||
message: `Semantic-layer search needs an initialized KTX project at ${projectDir}.`,
|
||||
nextSteps: baseNextSteps(projectDir, query),
|
||||
};
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ export function missingProjectSlSearchReadiness(
|
|||
export function noConnectionsSlSearchReadiness(
|
||||
projectDir: string,
|
||||
query: string | undefined,
|
||||
): KloAgentSlSearchReadinessDetail {
|
||||
): KtxAgentSlSearchReadinessDetail {
|
||||
return {
|
||||
code: 'agent_sl_search_no_connections',
|
||||
message: `Semantic-layer search found no configured connections in ${projectDir}.`,
|
||||
|
|
@ -54,7 +54,7 @@ export function missingConnectionSlSearchReadiness(
|
|||
projectDir: string,
|
||||
connectionId: string,
|
||||
query: string | undefined,
|
||||
): KloAgentSlSearchReadinessDetail {
|
||||
): KtxAgentSlSearchReadinessDetail {
|
||||
return {
|
||||
code: 'agent_sl_search_unknown_connection',
|
||||
message: `Semantic-layer search connection "${connectionId}" is not configured in ${projectDir}.`,
|
||||
|
|
@ -65,7 +65,7 @@ export function missingConnectionSlSearchReadiness(
|
|||
export function noIndexedSourcesSlSearchReadiness(
|
||||
projectDir: string,
|
||||
query: string | undefined,
|
||||
): KloAgentSlSearchReadinessDetail {
|
||||
): KtxAgentSlSearchReadinessDetail {
|
||||
return {
|
||||
code: 'agent_sl_search_no_indexed_sources',
|
||||
message: `Semantic-layer search found no indexed semantic-layer sources in ${projectDir}.`,
|
||||
|
|
@ -90,5 +90,5 @@ function errorPath(error: unknown): string | undefined {
|
|||
}
|
||||
|
||||
export function isMissingProjectConfigError(error: unknown): boolean {
|
||||
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('klo.yaml') ?? false);
|
||||
return errorCode(error) === 'ENOENT' && (errorPath(error)?.endsWith('ktx.yaml') ?? false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { buildDefaultKloProjectConfig } from '@klo/context/project';
|
||||
import { buildDefaultKtxProjectConfig } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runKloAgent } from './agent.js';
|
||||
import type { KloAgentRuntime } from './agent-runtime.js';
|
||||
import { runKtxAgent } from './agent.js';
|
||||
import type { KtxAgentRuntime } from './agent-runtime.js';
|
||||
|
||||
function makeIo() {
|
||||
let stdout = '';
|
||||
|
|
@ -19,21 +19,21 @@ function makeIo() {
|
|||
};
|
||||
}
|
||||
|
||||
function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
|
||||
const config = buildDefaultKloProjectConfig('revenue');
|
||||
function runtime(overrides: Record<string, unknown> = {}): KtxAgentRuntime {
|
||||
const config = buildDefaultKtxProjectConfig('revenue');
|
||||
return {
|
||||
project: {
|
||||
projectDir: '/tmp/revenue',
|
||||
configPath: '/tmp/revenue/klo.yaml',
|
||||
configPath: '/tmp/revenue/ktx.yaml',
|
||||
config: {
|
||||
...config,
|
||||
connections: {
|
||||
warehouse: { driver: 'sqlite', path: 'warehouse.sqlite', readonly: true as const },
|
||||
},
|
||||
},
|
||||
coreConfig: {} as KloAgentRuntime['project']['coreConfig'],
|
||||
git: {} as KloAgentRuntime['project']['git'],
|
||||
fileStore: {} as KloAgentRuntime['project']['fileStore'],
|
||||
coreConfig: {} as KtxAgentRuntime['project']['coreConfig'],
|
||||
git: {} as KtxAgentRuntime['project']['git'],
|
||||
fileStore: {} as KtxAgentRuntime['project']['fileStore'],
|
||||
},
|
||||
ports: {
|
||||
connections: { list: vi.fn(async () => [{ id: 'warehouse', name: 'warehouse', connectionType: 'sqlite' }]) },
|
||||
|
|
@ -86,7 +86,7 @@ function runtime(overrides: Record<string, unknown> = {}): KloAgentRuntime {
|
|||
};
|
||||
}
|
||||
|
||||
function runtimeWithoutConnections(): KloAgentRuntime {
|
||||
function runtimeWithoutConnections(): KtxAgentRuntime {
|
||||
const base = runtime();
|
||||
return {
|
||||
...base,
|
||||
|
|
@ -107,11 +107,11 @@ function runtimeWithoutConnections(): KloAgentRuntime {
|
|||
};
|
||||
}
|
||||
|
||||
describe('runKloAgent', () => {
|
||||
describe('runKtxAgent', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-agent-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-agent-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -121,7 +121,7 @@ describe('runKloAgent', () => {
|
|||
it('prints tool discovery with every stable command', async () => {
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKloAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
|
||||
await expect(runKtxAgent({ command: 'tools', projectDir: tempDir, json: true }, io.io)).resolves.toBe(0);
|
||||
|
||||
const body = JSON.parse(io.stdout());
|
||||
expect(body.projectDir).toBe(tempDir);
|
||||
|
|
@ -143,7 +143,7 @@ describe('runKloAgent', () => {
|
|||
const readSetupStatus = vi.fn(async () => ({ project: { path: tempDir, ready: true }, agents: [] }));
|
||||
|
||||
await expect(
|
||||
runKloAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
|
||||
runKtxAgent({ command: 'context', projectDir: tempDir, json: true }, io.io, { createRuntime, readSetupStatus }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(JSON.parse(io.stdout())).toMatchObject({
|
||||
|
|
@ -168,7 +168,7 @@ describe('runKloAgent', () => {
|
|||
{ command: 'wiki-read' as const, projectDir: tempDir, json: true as const, pageId: 'page-1' },
|
||||
]) {
|
||||
const io = makeIo();
|
||||
await expect(runKloAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
|
||||
await expect(runKtxAgent(args, io.io, { createRuntime: async () => runtime() })).resolves.toBe(0);
|
||||
expect(JSON.parse(io.stdout())).toBeTruthy();
|
||||
expect(io.stderr()).toBe('');
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
|
||||
runKtxAgent({ command: 'wiki-search', projectDir: tempDir, json: true, query: 'paid order', limit: 5 }, io.io, {
|
||||
createRuntime: async () => fakeRuntime,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -222,7 +222,7 @@ describe('runKloAgent', () => {
|
|||
await writeFile(queryFile, '{"measures":["total_revenue"],"dimensions":[]}', 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{
|
||||
command: 'sl-query',
|
||||
projectDir: tempDir,
|
||||
|
|
@ -247,7 +247,7 @@ describe('runKloAgent', () => {
|
|||
await writeFile(sqlFile, 'select 1', 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{
|
||||
command: 'sql-execute',
|
||||
projectDir: tempDir,
|
||||
|
|
@ -274,11 +274,11 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
const missingProjectError = Object.assign(new Error('ENOENT: no such file or directory'), {
|
||||
code: 'ENOENT',
|
||||
path: join(tempDir, 'klo.yaml'),
|
||||
path: join(tempDir, 'ktx.yaml'),
|
||||
});
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'gross revenue' },
|
||||
io.io,
|
||||
{ createRuntime: vi.fn(async () => Promise.reject(missingProjectError)) },
|
||||
|
|
@ -289,12 +289,12 @@ describe('runKloAgent', () => {
|
|||
ok: false,
|
||||
error: {
|
||||
code: 'agent_sl_search_missing_project',
|
||||
message: `Semantic-layer search needs an initialized KLO project at ${tempDir}.`,
|
||||
message: `Semantic-layer search needs an initialized KTX project at ${tempDir}.`,
|
||||
nextSteps: [
|
||||
'klo demo',
|
||||
`klo setup --project-dir ${tempDir}`,
|
||||
'klo ingest <connection>',
|
||||
`klo agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
|
||||
'ktx demo',
|
||||
`ktx setup --project-dir ${tempDir}`,
|
||||
'ktx ingest <connection>',
|
||||
`ktx agent sl list --json --query "gross revenue" --project-dir ${tempDir}`,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
@ -305,7 +305,7 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{ command: 'sl-list', projectDir: tempDir, json: true, query: 'revenue' },
|
||||
io.io,
|
||||
{ createRuntime: async () => runtimeWithoutConnections() },
|
||||
|
|
@ -318,10 +318,10 @@ describe('runKloAgent', () => {
|
|||
code: 'agent_sl_search_no_connections',
|
||||
message: `Semantic-layer search found no configured connections in ${tempDir}.`,
|
||||
nextSteps: [
|
||||
'klo demo',
|
||||
`klo setup --project-dir ${tempDir}`,
|
||||
'klo ingest <connection>',
|
||||
`klo agent sl list --json --query "revenue" --project-dir ${tempDir}`,
|
||||
'ktx demo',
|
||||
`ktx setup --project-dir ${tempDir}`,
|
||||
'ktx ingest <connection>',
|
||||
`ktx agent sl list --json --query "revenue" --project-dir ${tempDir}`,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
@ -331,7 +331,7 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'missing', query: 'revenue' },
|
||||
io.io,
|
||||
{ createRuntime: async () => runtime() },
|
||||
|
|
@ -357,7 +357,7 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloAgent(
|
||||
runKtxAgent(
|
||||
{ command: 'sl-list', projectDir: tempDir, json: true, connectionId: 'warehouse', query: 'revenue' },
|
||||
io.io,
|
||||
{ createRuntime: async () => fakeRuntime },
|
||||
|
|
@ -377,7 +377,7 @@ describe('runKloAgent', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
|
||||
runKtxAgent({ command: 'wiki-read', projectDir: tempDir, json: true, pageId: 'missing' }, io.io, {
|
||||
createRuntime: async () =>
|
||||
runtime({
|
||||
ports: { knowledge: { read: vi.fn(async () => null) } },
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import type { KloCliIo } from './cli-runtime.js';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import {
|
||||
createKloAgentRuntime,
|
||||
createKtxAgentRuntime,
|
||||
parseAgentMaxRows,
|
||||
readAgentJsonFile,
|
||||
writeAgentJson,
|
||||
writeAgentJsonError,
|
||||
type KloAgentRuntime,
|
||||
type KloAgentRuntimeDeps,
|
||||
type KtxAgentRuntime,
|
||||
type KtxAgentRuntimeDeps,
|
||||
} from './agent-runtime.js';
|
||||
import {
|
||||
isMissingProjectConfigError,
|
||||
|
|
@ -15,11 +15,11 @@ import {
|
|||
missingProjectSlSearchReadiness,
|
||||
noConnectionsSlSearchReadiness,
|
||||
noIndexedSourcesSlSearchReadiness,
|
||||
type KloAgentSlSearchReadinessDetail,
|
||||
type KtxAgentSlSearchReadinessDetail,
|
||||
} from './agent-search-readiness.js';
|
||||
import { readKloSetupStatus, type KloSetupStatus } from './setup.js';
|
||||
import { readKtxSetupStatus, type KtxSetupStatus } from './setup.js';
|
||||
|
||||
export type KloAgentArgs =
|
||||
export type KtxAgentArgs =
|
||||
| { command: 'tools'; projectDir: string; json: true }
|
||||
| { command: 'context'; projectDir: string; json: true }
|
||||
| { command: 'sl-list'; projectDir: string; json: true; connectionId?: string; query?: string }
|
||||
|
|
@ -37,38 +37,38 @@ export type KloAgentArgs =
|
|||
| { command: 'wiki-read'; projectDir: string; json: true; pageId: string }
|
||||
| { command: 'sql-execute'; projectDir: string; json: true; connectionId: string; sqlFile: string; maxRows?: number };
|
||||
|
||||
export interface KloAgentDeps extends KloAgentRuntimeDeps {
|
||||
export interface KtxAgentDeps extends KtxAgentRuntimeDeps {
|
||||
createRuntime?: (options: {
|
||||
projectDir: string;
|
||||
enableSemanticCompute: boolean;
|
||||
enableQueryExecution: boolean;
|
||||
}) => Promise<KloAgentRuntime>;
|
||||
}) => Promise<KtxAgentRuntime>;
|
||||
readSetupStatus?: (
|
||||
projectDir: string,
|
||||
) => Promise<KloSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
|
||||
) => Promise<KtxSetupStatus | { project: { path?: string; ready: boolean }; agents: unknown[] }>;
|
||||
}
|
||||
|
||||
const AGENT_TOOLS = [
|
||||
{ name: 'context', command: 'klo agent context --json' },
|
||||
{ name: 'sl.list', command: 'klo agent sl list --json [--connection-id <id>] [--query <text>]' },
|
||||
{ name: 'sl.read', command: 'klo agent sl read <sourceName> --json [--connection-id <id>]' },
|
||||
{ name: 'context', command: 'ktx agent context --json' },
|
||||
{ name: 'sl.list', command: 'ktx agent sl list --json [--connection-id <id>] [--query <text>]' },
|
||||
{ name: 'sl.read', command: 'ktx agent sl read <sourceName> --json [--connection-id <id>]' },
|
||||
{
|
||||
name: 'sl.query',
|
||||
command: 'klo agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
|
||||
command: 'ktx agent sl query --json --connection-id <id> --query-file <path> --execute --max-rows 100',
|
||||
},
|
||||
{ name: 'wiki.search', command: 'klo agent wiki search <query> --json [--limit 10]' },
|
||||
{ name: 'wiki.read', command: 'klo agent wiki read <pageId> --json' },
|
||||
{ name: 'wiki.search', command: 'ktx agent wiki search <query> --json [--limit 10]' },
|
||||
{ name: 'wiki.read', command: 'ktx agent wiki read <pageId> --json' },
|
||||
{
|
||||
name: 'sql.execute',
|
||||
command: 'klo agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
|
||||
command: 'ktx agent sql execute --json --connection-id <id> --sql-file <path> --max-rows 100',
|
||||
},
|
||||
] as const;
|
||||
|
||||
function writeAgentSlSearchReadinessError(io: KloCliIo, detail: KloAgentSlSearchReadinessDetail): void {
|
||||
function writeAgentSlSearchReadinessError(io: KtxCliIo, detail: KtxAgentSlSearchReadinessDetail): void {
|
||||
writeAgentJsonError(io, detail.message, { code: detail.code, nextSteps: detail.nextSteps });
|
||||
}
|
||||
|
||||
async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAgentRuntime> {
|
||||
async function runtimeFor(args: KtxAgentArgs, deps: KtxAgentDeps): Promise<KtxAgentRuntime> {
|
||||
const needsSemanticCompute = args.command === 'sl-query';
|
||||
const needsQueryExecution = args.command === 'sql-execute' || (args.command === 'sl-query' && args.execute);
|
||||
return deps.createRuntime
|
||||
|
|
@ -77,7 +77,7 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
|
|||
enableSemanticCompute: needsSemanticCompute,
|
||||
enableQueryExecution: needsQueryExecution,
|
||||
})
|
||||
: createKloAgentRuntime(
|
||||
: createKtxAgentRuntime(
|
||||
{
|
||||
projectDir: args.projectDir,
|
||||
enableSemanticCompute: needsSemanticCompute,
|
||||
|
|
@ -87,14 +87,14 @@ async function runtimeFor(args: KloAgentArgs, deps: KloAgentDeps): Promise<KloAg
|
|||
);
|
||||
}
|
||||
|
||||
function connectionIdForSource(runtime: KloAgentRuntime, requested: string | undefined): string {
|
||||
function connectionIdForSource(runtime: KtxAgentRuntime, requested: string | undefined): string {
|
||||
if (requested) return requested;
|
||||
const ids = Object.keys(runtime.project.config.connections ?? {});
|
||||
if (ids.length === 1) return ids[0] as string;
|
||||
throw new Error('Use --connection-id when the project has zero or multiple connections.');
|
||||
}
|
||||
|
||||
export async function runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAgentDeps = {}): Promise<number> {
|
||||
export async function runKtxAgent(args: KtxAgentArgs, io: KtxCliIo, deps: KtxAgentDeps = {}): Promise<number> {
|
||||
try {
|
||||
if (args.command === 'tools') {
|
||||
writeAgentJson(io, { projectDir: args.projectDir, tools: AGENT_TOOLS });
|
||||
|
|
@ -105,7 +105,7 @@ export async function runKloAgent(args: KloAgentArgs, io: KloCliIo, deps: KloAge
|
|||
|
||||
if (args.command === 'context') {
|
||||
const [status, connections, semanticLayer] = await Promise.all([
|
||||
(deps.readSetupStatus ?? readKloSetupStatus)(args.projectDir),
|
||||
(deps.readSetupStatus ?? readKtxSetupStatus)(args.projectDir),
|
||||
runtime.ports.connections?.list() ?? [],
|
||||
runtime.ports.semanticLayer?.listSources({}) ?? { sources: [], totalSources: 0 },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ import { installStartupProfileReporter, profileMark, profileSpan } from './start
|
|||
|
||||
installStartupProfileReporter();
|
||||
profileMark('bin:entry');
|
||||
const { runKloCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
|
||||
profileMark('bin:runKloCli');
|
||||
process.exitCode = await runKloCli(process.argv.slice(2));
|
||||
const { runKtxCli } = await profileSpan('import ./cli-runtime.js', () => import('./cli-runtime.js'));
|
||||
profileMark('bin:runKtxCli');
|
||||
process.exitCode = await runKtxCli(process.argv.slice(2));
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { spinner } from '@clack/prompts';
|
||||
|
||||
export interface KloCliSpinner {
|
||||
export interface KtxCliSpinner {
|
||||
start(message: string): void;
|
||||
stop(message: string): void;
|
||||
error(message: string): void;
|
||||
}
|
||||
|
||||
export function createClackSpinner(): KloCliSpinner {
|
||||
export function createClackSpinner(): KtxCliSpinner {
|
||||
return spinner();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
|
||||
import type { KloCliDeps, KloCliIo, KloCliPackageInfo } from './cli-runtime.js';
|
||||
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
|
||||
import { registerAgentCommands } from './commands/agent-commands.js';
|
||||
import { registerConnectionCommands } from './commands/connection-commands.js';
|
||||
import { registerWikiCommands } from './commands/knowledge-commands.js';
|
||||
|
|
@ -9,16 +9,16 @@ import { registerSetupCommands } from './commands/setup-commands.js';
|
|||
import { registerSlCommands } from './commands/sl-commands.js';
|
||||
import { registerStatusCommands } from './commands/status-commands.js';
|
||||
import { registerDevCommands } from './dev.js';
|
||||
import { findNearestKloProjectDir, resolveKloProjectDir } from './project-resolver.js';
|
||||
import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
|
||||
import { profileMark, profileSpan } from './startup-profile.js';
|
||||
|
||||
profileMark('module:cli-program');
|
||||
|
||||
export interface KloCliCommandContext {
|
||||
io: KloCliIo;
|
||||
deps: KloCliDeps;
|
||||
export interface KtxCliCommandContext {
|
||||
io: KtxCliIo;
|
||||
deps: KtxCliDeps;
|
||||
setExitCode: (code: number) => void;
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
writeDebug?: (command: string, commandContext: CommandWithGlobalOptions) => void;
|
||||
}
|
||||
|
||||
|
|
@ -29,13 +29,13 @@ export interface OutputModeOptions {
|
|||
input?: boolean;
|
||||
}
|
||||
|
||||
interface KloCommanderProgramOptions {
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KloCliIo) => Promise<number>;
|
||||
interface KtxCommanderProgramOptions {
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
}
|
||||
|
||||
type CommanderExitLike = { exitCode: number; code: string; message: string };
|
||||
|
||||
interface KloGlobalOptionValues {
|
||||
interface KtxGlobalOptionValues {
|
||||
projectDir?: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ export function parseNonEmptyAssignmentOption(value: string): { key: string; val
|
|||
};
|
||||
}
|
||||
|
||||
function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionValues {
|
||||
function optionsWithGlobals(command: CommandWithGlobalOptions): KtxGlobalOptionValues {
|
||||
const options = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
|
||||
const values = options as { projectDir?: unknown; debug?: unknown };
|
||||
return {
|
||||
|
|
@ -114,25 +114,25 @@ function optionsWithGlobals(command: CommandWithGlobalOptions): KloGlobalOptionV
|
|||
}
|
||||
|
||||
export function resolveCommandProjectDir(command: CommandWithGlobalOptions): string {
|
||||
return resolveKloProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
|
||||
return resolveKtxProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
|
||||
}
|
||||
|
||||
export function resolveCommandProjectDirOverride(command: CommandWithGlobalOptions): string | undefined {
|
||||
return optionsWithGlobals(command).projectDir ?? process.env.KLO_PROJECT_DIR;
|
||||
return optionsWithGlobals(command).projectDir ?? process.env.KTX_PROJECT_DIR;
|
||||
}
|
||||
|
||||
function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
|
||||
function createBaseProgram(info: KtxCliPackageInfo, io: KtxCliIo): Command {
|
||||
return new Command()
|
||||
.name('klo')
|
||||
.description('Standalone KLO developer CLI')
|
||||
.option('--project-dir <path>', 'KLO project directory (default: KLO_PROJECT_DIR, nearest klo.yaml, or cwd)')
|
||||
.name('ktx')
|
||||
.description('Standalone KTX developer CLI')
|
||||
.option('--project-dir <path>', 'KTX project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
|
||||
.option('--debug', 'Enable diagnostic logging to stderr')
|
||||
.version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
|
||||
.helpOption('-h, --help', 'Show this help text')
|
||||
.configureHelp({ showGlobalOptions: true })
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nAdvanced:\n klo dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
|
||||
'\nAdvanced:\n ktx dev Low-level diagnostics, scans, adapter commands, and mapping tools.\n',
|
||||
)
|
||||
.showHelpAfterError()
|
||||
.exitOverride()
|
||||
|
|
@ -143,7 +143,7 @@ function createBaseProgram(info: KloCliPackageInfo, io: KloCliIo): Command {
|
|||
});
|
||||
}
|
||||
|
||||
function writeDebug(io: KloCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
|
||||
function writeDebug(io: KtxCliIo, commandContext: CommandWithGlobalOptions, command: string): void {
|
||||
const global = optionsWithGlobals(commandContext);
|
||||
if (global.debug !== true) {
|
||||
return;
|
||||
|
|
@ -158,18 +158,18 @@ function formatCliError(error: unknown): string {
|
|||
|
||||
async function runBareInteractiveCommand(
|
||||
program: Command,
|
||||
io: KloCliIo,
|
||||
context: KloCliCommandContext,
|
||||
io: KtxCliIo,
|
||||
context: KtxCliCommandContext,
|
||||
): Promise<number> {
|
||||
const nearestProjectDir = findNearestKloProjectDir(process.cwd());
|
||||
const envProjectDir = process.env.KLO_PROJECT_DIR;
|
||||
const runner = context.deps.setup ?? (await import('./setup.js')).runKloSetup;
|
||||
const nearestProjectDir = findNearestKtxProjectDir(process.cwd());
|
||||
const envProjectDir = process.env.KTX_PROJECT_DIR;
|
||||
const runner = context.deps.setup ?? (await import('./setup.js')).runKtxSetup;
|
||||
|
||||
if (!nearestProjectDir && !envProjectDir) {
|
||||
return await runner(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: resolveKloProjectDir(),
|
||||
projectDir: resolveKtxProjectDir(),
|
||||
mode: 'auto',
|
||||
agents: false,
|
||||
agentScope: 'project',
|
||||
|
|
@ -191,18 +191,18 @@ async function runBareInteractiveCommand(
|
|||
return 0;
|
||||
}
|
||||
|
||||
export async function runCommanderKloCli(
|
||||
export async function runCommanderKtxCli(
|
||||
argv: string[],
|
||||
io: KloCliIo,
|
||||
deps: KloCliDeps,
|
||||
info: KloCliPackageInfo,
|
||||
options: KloCommanderProgramOptions,
|
||||
io: KtxCliIo,
|
||||
deps: KtxCliDeps,
|
||||
info: KtxCliPackageInfo,
|
||||
options: KtxCommanderProgramOptions,
|
||||
): Promise<number> {
|
||||
profileMark('commander:entry');
|
||||
let exitCode = 0;
|
||||
const program = createBaseProgram(info, io);
|
||||
profileMark('commander:base-program');
|
||||
const context: KloCliCommandContext = {
|
||||
const context: KtxCliCommandContext = {
|
||||
io,
|
||||
deps,
|
||||
setExitCode: (code: number) => {
|
||||
|
|
|
|||
|
|
@ -1,67 +1,67 @@
|
|||
import type { KloConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
|
||||
import type { KloConnectionNotionArgs } from './commands/connection-notion.js';
|
||||
import type { KloAgentArgs } from './agent.js';
|
||||
import type { KloConnectionArgs } from './connection.js';
|
||||
import type { KloDemoArgs } from './demo.js';
|
||||
import type { KloDoctorArgs } from './doctor.js';
|
||||
import type { KloIngestArgs } from './ingest.js';
|
||||
import type { KloKnowledgeArgs } from './knowledge.js';
|
||||
import type { KloPublicIngestArgs } from './public-ingest.js';
|
||||
import type { KloScanArgs } from './scan.js';
|
||||
import type { KloServeArgs } from './serve.js';
|
||||
import type { KloSetupArgs } from './setup.js';
|
||||
import type { KloSlArgs } from './sl.js';
|
||||
import type { KtxConnectionMetabaseSetupArgs } from './commands/connection-metabase-setup.js';
|
||||
import type { KtxConnectionNotionArgs } from './commands/connection-notion.js';
|
||||
import type { KtxAgentArgs } from './agent.js';
|
||||
import type { KtxConnectionArgs } from './connection.js';
|
||||
import type { KtxDemoArgs } from './demo.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 { KtxScanArgs } from './scan.js';
|
||||
import type { KtxServeArgs } from './serve.js';
|
||||
import type { KtxSetupArgs } from './setup.js';
|
||||
import type { KtxSlArgs } from './sl.js';
|
||||
import { profileMark, profileSpan } from './startup-profile.js';
|
||||
|
||||
profileMark('module:cli-runtime');
|
||||
|
||||
export interface KloCliPackageInfo {
|
||||
name: '@klo/cli';
|
||||
export interface KtxCliPackageInfo {
|
||||
name: '@ktx/cli';
|
||||
version: '0.0.0-private';
|
||||
contextPackageName: '@klo/context';
|
||||
contextPackageName: '@ktx/context';
|
||||
}
|
||||
|
||||
export interface KloCliIo {
|
||||
export interface KtxCliIo {
|
||||
stdout: { isTTY?: boolean; write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
}
|
||||
|
||||
export interface KloCliDeps {
|
||||
serveStdio?: (args: KloServeArgs) => Promise<number>;
|
||||
setup?: (args: KloSetupArgs, io: KloCliIo) => Promise<number>;
|
||||
agent?: (args: KloAgentArgs, io: KloCliIo) => Promise<number>;
|
||||
connection?: (args: KloConnectionArgs, io: KloCliIo) => Promise<number>;
|
||||
connectionNotion?: (args: KloConnectionNotionArgs, io: KloCliIo) => Promise<number>;
|
||||
connectionMetabaseSetup?: (args: KloConnectionMetabaseSetupArgs, io: KloCliIo) => Promise<number>;
|
||||
demo?: (args: KloDemoArgs, io: KloCliIo) => Promise<number>;
|
||||
doctor?: (args: KloDoctorArgs, io: KloCliIo) => Promise<number>;
|
||||
ingest?: (args: KloIngestArgs, io: KloCliIo) => Promise<number>;
|
||||
publicIngest?: (args: KloPublicIngestArgs, io: KloCliIo) => Promise<number>;
|
||||
scan?: (args: KloScanArgs, io: KloCliIo) => Promise<number>;
|
||||
knowledge?: (args: KloKnowledgeArgs, io: KloCliIo) => Promise<number>;
|
||||
sl?: (args: KloSlArgs, io: KloCliIo) => Promise<number>;
|
||||
export interface KtxCliDeps {
|
||||
serveStdio?: (args: KtxServeArgs) => Promise<number>;
|
||||
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
|
||||
agent?: (args: KtxAgentArgs, io: KtxCliIo) => Promise<number>;
|
||||
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
|
||||
connectionNotion?: (args: KtxConnectionNotionArgs, io: KtxCliIo) => Promise<number>;
|
||||
connectionMetabaseSetup?: (args: KtxConnectionMetabaseSetupArgs, io: KtxCliIo) => Promise<number>;
|
||||
demo?: (args: KtxDemoArgs, 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>;
|
||||
scan?: (args: KtxScanArgs, io: KtxCliIo) => Promise<number>;
|
||||
knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise<number>;
|
||||
sl?: (args: KtxSlArgs, io: KtxCliIo) => Promise<number>;
|
||||
}
|
||||
|
||||
export function getKloCliPackageInfo(): KloCliPackageInfo {
|
||||
export function getKtxCliPackageInfo(): KtxCliPackageInfo {
|
||||
return {
|
||||
name: '@klo/cli',
|
||||
name: '@ktx/cli',
|
||||
version: '0.0.0-private',
|
||||
contextPackageName: '@klo/context',
|
||||
contextPackageName: '@ktx/context',
|
||||
};
|
||||
}
|
||||
|
||||
async function runInit(
|
||||
args: { projectDir: string; projectName?: string; force: boolean },
|
||||
io: KloCliIo,
|
||||
io: KtxCliIo,
|
||||
): Promise<number> {
|
||||
const { initKloProject } = await import('@klo/context/project');
|
||||
const result = await initKloProject({
|
||||
const { initKtxProject } = await import('@ktx/context/project');
|
||||
const result = await initKtxProject({
|
||||
projectDir: args.projectDir,
|
||||
projectName: args.projectName,
|
||||
force: args.force,
|
||||
});
|
||||
|
||||
io.stdout.write(`Initialized KLO project at ${result.projectDir}\n`);
|
||||
io.stdout.write(`Initialized KTX project at ${result.projectDir}\n`);
|
||||
io.stdout.write(`Config: ${result.configPath}\n`);
|
||||
io.stdout.write(`Commit: ${result.commitHash ?? 'none'}\n`);
|
||||
return 0;
|
||||
|
|
@ -69,21 +69,21 @@ async function runInit(
|
|||
|
||||
export async function runInitForCommander(
|
||||
args: { projectDir: string; projectName?: string; force: boolean },
|
||||
io: KloCliIo,
|
||||
io: KtxCliIo,
|
||||
): Promise<number> {
|
||||
return await runInit(args, io);
|
||||
}
|
||||
|
||||
export async function runKloCli(
|
||||
export async function runKtxCli(
|
||||
argv = process.argv.slice(2),
|
||||
io: KloCliIo = process,
|
||||
deps: KloCliDeps = {},
|
||||
io: KtxCliIo = process,
|
||||
deps: KtxCliDeps = {},
|
||||
): Promise<number> {
|
||||
const info = getKloCliPackageInfo();
|
||||
profileMark('runtime:runKloCli');
|
||||
const { runCommanderKloCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
|
||||
const info = getKtxCliPackageInfo();
|
||||
profileMark('runtime:runKtxCli');
|
||||
const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
|
||||
|
||||
return await runCommanderKloCli(argv, io, deps, info, {
|
||||
return await runCommanderKtxCli(argv, io, deps, info, {
|
||||
runInit: runInitForCommander,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Option, type Command } from '@commander-js/extra-typings';
|
||||
import type { KloAgentArgs } from '../agent.js';
|
||||
import type { KloCliCommandContext } from '../cli-program.js';
|
||||
import type { KtxAgentArgs } from '../agent.js';
|
||||
import type { KtxCliCommandContext } from '../cli-program.js';
|
||||
import { parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
|
||||
|
||||
async function runAgent(context: KloCliCommandContext, args: KloAgentArgs): Promise<void> {
|
||||
const runner = context.deps.agent ?? (await import('../agent.js')).runKloAgent;
|
||||
async function runAgent(context: KtxCliCommandContext, args: KtxAgentArgs): Promise<void> {
|
||||
const runner = context.deps.agent ?? (await import('../agent.js')).runKtxAgent;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
|
|
@ -12,10 +12,10 @@ function jsonOption(): Option {
|
|||
return new Option('--json', 'Print JSON output').makeOptionMandatory();
|
||||
}
|
||||
|
||||
export function registerAgentCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerAgentCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const agent = program
|
||||
.command('agent', { hidden: true })
|
||||
.description('Machine-readable KLO commands for coding agents')
|
||||
.description('Machine-readable KTX commands for coding agents')
|
||||
.showHelpAfterError();
|
||||
|
||||
agent.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
|
|
@ -24,7 +24,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
|
|||
|
||||
agent
|
||||
.command('tools')
|
||||
.description('Print available agent-facing KLO tools')
|
||||
.description('Print available agent-facing KTX tools')
|
||||
.addOption(jsonOption())
|
||||
.action(async (_options, command) => {
|
||||
await runAgent(context, { command: 'tools', projectDir: resolveCommandProjectDir(command), json: true });
|
||||
|
|
@ -91,10 +91,10 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
|
|||
},
|
||||
);
|
||||
|
||||
const wiki = agent.command('wiki').description('KLO wiki agent commands');
|
||||
const wiki = agent.command('wiki').description('KTX wiki agent commands');
|
||||
wiki
|
||||
.command('search')
|
||||
.description('Search KLO wiki pages')
|
||||
.description('Search KTX wiki pages')
|
||||
.argument('<query>')
|
||||
.addOption(jsonOption())
|
||||
.option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption, 10)
|
||||
|
|
@ -109,7 +109,7 @@ export function registerAgentCommands(program: Command, context: KloCliCommandCo
|
|||
});
|
||||
wiki
|
||||
.command('read')
|
||||
.description('Read one KLO wiki page')
|
||||
.description('Read one KTX wiki page')
|
||||
.argument('<pageId>')
|
||||
.addOption(jsonOption())
|
||||
.action(async (pageId: string, _options, command) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { CommandUnknownOpts } from '@commander-js/extra-typings';
|
||||
import type { KloCliCommandContext } from '../cli-program.js';
|
||||
import type { KtxCliCommandContext } from '../cli-program.js';
|
||||
import { completeCommanderInput, installZshCompletion, zshCompletionScript } from '../completion.js';
|
||||
|
||||
export function registerCompletionCommands(
|
||||
program: CommandUnknownOpts,
|
||||
context: KloCliCommandContext,
|
||||
context: KtxCliCommandContext,
|
||||
completionRoot: CommandUnknownOpts = program,
|
||||
): void {
|
||||
program
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
||||
import {
|
||||
collectOption,
|
||||
type KloCliCommandContext,
|
||||
type KtxCliCommandContext,
|
||||
parseBooleanStringOption,
|
||||
parseNonEmptyAssignmentOption,
|
||||
parseNonNegativeIntegerOption,
|
||||
|
|
@ -10,9 +10,9 @@ import {
|
|||
resolveCommandProjectDir,
|
||||
} from '../cli-program.js';
|
||||
import { connectionAddCommandSchema } from '../command-schemas.js';
|
||||
import type { KloConnectionArgs } from '../connection.js';
|
||||
import type { KtxConnectionArgs } from '../connection.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
import type { KloConnectionMappingArgs } from './connection-mapping.js';
|
||||
import type { KtxConnectionMappingArgs } from './connection-mapping.js';
|
||||
import { registerConnectionMetabaseCommands } from './connection-metabase-commands.js';
|
||||
import { registerConnectionNotionCommands } from './connection-notion-commands.js';
|
||||
|
||||
|
|
@ -42,24 +42,24 @@ function parseMappingFieldOption(value: string): 'databaseMappings' | 'connectio
|
|||
throw new InvalidArgumentError('must be databaseMappings or connectionMappings');
|
||||
}
|
||||
|
||||
async function runConnectionArgs(context: KloCliCommandContext, args: KloConnectionArgs): Promise<void> {
|
||||
const runner = context.deps.connection ?? (await import('../connection.js')).runKloConnection;
|
||||
async function runConnectionArgs(context: KtxCliCommandContext, args: KtxConnectionArgs): Promise<void> {
|
||||
const runner = context.deps.connection ?? (await import('../connection.js')).runKtxConnection;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
async function runMappingArgs(context: KloCliCommandContext, args: KloConnectionMappingArgs): Promise<void> {
|
||||
const { runKloConnectionMapping } = await import('./connection-mapping.js');
|
||||
context.setExitCode(await runKloConnectionMapping(args, context.io));
|
||||
async function runMappingArgs(context: KtxCliCommandContext, args: KtxConnectionMappingArgs): Promise<void> {
|
||||
const { runKtxConnectionMapping } = await import('./connection-mapping.js');
|
||||
context.setExitCode(await runKtxConnectionMapping(args, context.io));
|
||||
}
|
||||
|
||||
export function registerConnectionCommands(program: Command, context: KloCliCommandContext, commandName = 'connection'): void {
|
||||
export function registerConnectionCommands(program: Command, context: KtxCliCommandContext, commandName = 'connection'): void {
|
||||
const connection = program
|
||||
.command(commandName)
|
||||
.description('Add, list, test, and map data sources')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the nearest klo.yaml or current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n',
|
||||
);
|
||||
connection.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
context.writeDebug?.(commandName, actionCommand);
|
||||
|
|
@ -75,7 +75,7 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
|
|||
connection
|
||||
.command('test')
|
||||
.description('Test a configured connection')
|
||||
.argument('<connectionId>', 'KLO connection id')
|
||||
.argument('<connectionId>', 'KTX connection id')
|
||||
.action(async (connectionId: string, _options: unknown, command) => {
|
||||
await runConnectionArgs(context, {
|
||||
command: 'test',
|
||||
|
|
@ -88,12 +88,12 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
|
|||
.command('add')
|
||||
.description('Add or replace a configured connection')
|
||||
.argument('<driver>', 'Connection driver')
|
||||
.argument('<connectionId>', 'KLO connection id')
|
||||
.argument('<connectionId>', 'KTX connection id')
|
||||
.option('--url <url>', 'Connection URL, env:NAME, or file:/path reference')
|
||||
.option('--schema <schema>', 'Schema to include; repeatable', collectOption, [])
|
||||
.option('--readonly', 'Mark the connection as read-only', false)
|
||||
.option('--force', 'Replace an existing connection', false)
|
||||
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to klo.yaml', false)
|
||||
.option('--allow-literal-credentials', 'Allow writing a literal credential URL to ktx.yaml', false)
|
||||
.addOption(new Option('--token-env <name>', 'Environment variable containing Notion auth token').conflicts('tokenFile'))
|
||||
.addOption(new Option('--token-file <path>', 'File containing Notion auth token').conflicts('tokenEnv'))
|
||||
.addOption(
|
||||
|
|
@ -155,8 +155,8 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
|
|||
|
||||
connection
|
||||
.command('remove')
|
||||
.description('Remove a configured connection from klo.yaml')
|
||||
.argument('<connectionId>', 'KLO connection id')
|
||||
.description('Remove a configured connection from ktx.yaml')
|
||||
.argument('<connectionId>', 'KTX connection id')
|
||||
.option('--force', 'Remove without prompting', false)
|
||||
.option('--no-input', 'Disable interactive terminal input')
|
||||
.action(async (connectionId: string, options: { force?: boolean; input?: boolean }, command) => {
|
||||
|
|
@ -188,14 +188,14 @@ export function registerConnectionCommands(program: Command, context: KloCliComm
|
|||
registerConnectionNotionCommands(connection, context);
|
||||
}
|
||||
|
||||
export function registerConnectionMappingCommands(connection: Command, context: KloCliCommandContext): void {
|
||||
export function registerConnectionMappingCommands(connection: Command, context: KtxCliCommandContext): void {
|
||||
const mapping = connection
|
||||
.command('mapping')
|
||||
.description('Manage Metabase warehouse mappings')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
mapping
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
|
||||
import { initKloProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
|
||||
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
|
||||
import { initKtxProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runKloConnectionMapping } from './connection-mapping.js';
|
||||
import { runKtxConnectionMapping } from './connection-mapping.js';
|
||||
|
||||
function makeIo() {
|
||||
let stdout = '';
|
||||
|
|
@ -27,18 +27,18 @@ function makeIo() {
|
|||
};
|
||||
}
|
||||
|
||||
describe('runKloConnectionMapping', () => {
|
||||
describe('runKtxConnectionMapping', () => {
|
||||
let tempDir: string;
|
||||
let projectDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-mapping-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-mapping-'));
|
||||
projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'mapping' });
|
||||
const project = await loadKloProject({ projectDir });
|
||||
await initKtxProject({ projectDir, projectName: 'mapping' });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig({
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
connections: {
|
||||
'prod-metabase': {
|
||||
|
|
@ -53,22 +53,22 @@ describe('runKloConnectionMapping', () => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Seed Metabase mapping test connections',
|
||||
);
|
||||
});
|
||||
|
||||
async function replaceConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig({
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
connections,
|
||||
}),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Replace mapping test connections',
|
||||
);
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ describe('runKloConnectionMapping', () => {
|
|||
it('sets, lists, disables, and clears local Metabase mappings', async () => {
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'set',
|
||||
projectDir,
|
||||
|
|
@ -95,13 +95,13 @@ describe('runKloConnectionMapping', () => {
|
|||
|
||||
const listIo = makeIo();
|
||||
await expect(
|
||||
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
|
||||
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-metabase', json: false }, listIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(listIo.stdout()).toContain('1 -> prod-warehouse');
|
||||
expect(listIo.stdout()).toContain('unhydrated');
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'set-sync-enabled',
|
||||
projectDir,
|
||||
|
|
@ -114,7 +114,7 @@ describe('runKloConnectionMapping', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'clear',
|
||||
projectDir,
|
||||
|
|
@ -127,12 +127,12 @@ describe('runKloConnectionMapping', () => {
|
|||
});
|
||||
|
||||
it('lists Metabase yaml mapping bootstrap rows before any SQLite command writes', async () => {
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-yaml-mapping-'));
|
||||
await initKloProject({ projectDir, projectName: 'yaml-mapping' });
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-yaml-mapping-'));
|
||||
await initKtxProject({ projectDir, projectName: 'yaml-mapping' });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig({
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
connections: {
|
||||
'prod-metabase': {
|
||||
|
|
@ -145,21 +145,21 @@ describe('runKloConnectionMapping', () => {
|
|||
'prod-warehouse': { driver: 'postgres', url: 'postgresql://readonly@db.test/analytics' },
|
||||
},
|
||||
}),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Seed yaml mappings',
|
||||
);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{ command: 'list', projectDir, connectionId: 'prod-metabase', json: false },
|
||||
io.io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('1 -> prod-warehouse');
|
||||
expect(io.stdout()).toContain('source: klo.yaml');
|
||||
expect(io.stdout()).toContain('source: ktx.yaml');
|
||||
});
|
||||
|
||||
it('refreshes Metabase discovery metadata through the injected runtime client', async () => {
|
||||
|
|
@ -178,7 +178,7 @@ describe('runKloConnectionMapping', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'refresh',
|
||||
projectDir,
|
||||
|
|
@ -194,7 +194,7 @@ describe('runKloConnectionMapping', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Discovery: 1 database');
|
||||
expect(client.cleanup).toHaveBeenCalledTimes(1);
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.klo', 'db.sqlite') });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: join(projectDir, '.ktx', 'db.sqlite') });
|
||||
await expect(store.listDatabaseMappings('prod-metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 1, metabaseDatabaseName: 'Analytics', source: 'refresh' },
|
||||
]);
|
||||
|
|
@ -215,7 +215,7 @@ describe('runKloConnectionMapping', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'set',
|
||||
projectDir,
|
||||
|
|
@ -228,7 +228,7 @@ describe('runKloConnectionMapping', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
|
||||
runKtxConnectionMapping({ command: 'list', projectDir, connectionId: 'prod-looker', json: false }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('analytics -> prod-warehouse');
|
||||
|
|
@ -242,7 +242,7 @@ describe('runKloConnectionMapping', () => {
|
|||
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{
|
||||
command: 'set',
|
||||
projectDir,
|
||||
|
|
@ -273,7 +273,7 @@ describe('runKloConnectionMapping', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping(
|
||||
runKtxConnectionMapping(
|
||||
{ command: 'refresh', projectDir, connectionId: 'prod-looker', autoAccept: true },
|
||||
io.io,
|
||||
{
|
||||
|
|
@ -298,12 +298,12 @@ describe('runKloConnectionMapping', () => {
|
|||
});
|
||||
|
||||
it('validates Looker mappings through the canonical local warehouse descriptor', async () => {
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'klo-cli-descriptor-validation-'));
|
||||
await initKloProject({ projectDir, projectName: 'descriptor-validation' });
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-cli-descriptor-validation-'));
|
||||
await initKtxProject({ projectDir, projectName: 'descriptor-validation' });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig({
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
connections: {
|
||||
'prod-looker': {
|
||||
|
|
@ -313,14 +313,14 @@ describe('runKloConnectionMapping', () => {
|
|||
'prod-warehouse': { driver: 'postgresql', url: 'postgresql://readonly@db.test/analytics' },
|
||||
},
|
||||
}),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Seed descriptor validation',
|
||||
);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
|
||||
runKtxConnectionMapping({ command: 'validate', projectDir, connectionId: 'prod-looker' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Mapping validation passed: prod-looker');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
|
||||
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
|
||||
import {
|
||||
DEFAULT_METABASE_CLIENT_CONFIG,
|
||||
DefaultLookerConnectionClientFactory,
|
||||
|
|
@ -12,20 +12,20 @@ import {
|
|||
discoverMetabaseDatabases,
|
||||
lookerCredentialsFromLocalConnection,
|
||||
metabaseRuntimeConfigFromLocalConnection,
|
||||
seedLocalMappingStateFromKloYaml,
|
||||
seedLocalMappingStateFromKtxYaml,
|
||||
validateLookerMappings,
|
||||
validateMappingPhysicalMatch,
|
||||
type LookerMappingClient,
|
||||
type MetabaseRuntimeClient,
|
||||
type MetabaseSyncMode,
|
||||
} from '@klo/context/ingest';
|
||||
import { type KloLocalProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
|
||||
import type { KloCliIo } from '../index.js';
|
||||
} from '@ktx/context/ingest';
|
||||
import { type KtxLocalProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
|
||||
import type { KtxCliIo } from '../index.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/connection-mapping');
|
||||
|
||||
export type KloConnectionMappingArgs =
|
||||
export type KtxConnectionMappingArgs =
|
||||
| { command: 'list'; projectDir: string; connectionId: string; json: boolean }
|
||||
| {
|
||||
command: 'set';
|
||||
|
|
@ -57,13 +57,13 @@ export type KloConnectionMappingArgs =
|
|||
| { command: 'validate'; projectDir: string; connectionId: string }
|
||||
| { command: 'clear'; projectDir: string; connectionId: string; metabaseDatabaseId?: number; mappingKey?: string };
|
||||
|
||||
interface KloConnectionMappingDeps {
|
||||
interface KtxConnectionMappingDeps {
|
||||
createMetabaseClient?: (
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
) => Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>>;
|
||||
createLookerClient?: (
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
) => Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }>;
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ function parseId(value: string, label: string): number {
|
|||
}
|
||||
|
||||
async function createDefaultMetabaseClient(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
): Promise<Pick<MetabaseRuntimeClient, 'getDatabases' | 'cleanup'>> {
|
||||
const factory = new DefaultMetabaseConnectionClientFactory(
|
||||
|
|
@ -97,7 +97,7 @@ async function createDefaultMetabaseClient(
|
|||
}
|
||||
|
||||
async function createDefaultLookerClient(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
): Promise<Pick<LookerMappingClient, 'listLookerConnections'> & { cleanup?(): Promise<void> }> {
|
||||
const factory = new DefaultLookerConnectionClientFactory({
|
||||
|
|
@ -110,30 +110,30 @@ async function createDefaultLookerClient(
|
|||
};
|
||||
}
|
||||
|
||||
function isLookerConnection(project: KloLocalProject, connectionId: string): boolean {
|
||||
function isLookerConnection(project: KtxLocalProject, connectionId: string): boolean {
|
||||
return String(project.config.connections[connectionId]?.driver ?? '').toLowerCase() === 'looker';
|
||||
}
|
||||
|
||||
function assertLookerConnection(project: KloLocalProject, connectionId: string): void {
|
||||
function assertLookerConnection(project: KtxLocalProject, connectionId: string): void {
|
||||
if (!isLookerConnection(project, connectionId)) {
|
||||
throw new Error(`Connection "${connectionId}" is not a Looker connection`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertMetabaseConnection(project: KloLocalProject, connectionId: string): void {
|
||||
function assertMetabaseConnection(project: KtxLocalProject, connectionId: string): void {
|
||||
const connection = project.config.connections[connectionId];
|
||||
if (!connection || String(connection.driver).toLowerCase() !== 'metabase') {
|
||||
throw new Error(`Connection "${connectionId}" is not a Metabase connection`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertTargetConnection(project: KloLocalProject, connectionId: string): void {
|
||||
function assertTargetConnection(project: KtxLocalProject, connectionId: string): void {
|
||||
if (!project.config.connections[connectionId]) {
|
||||
throw new Error(`Target connection "${connectionId}" does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
|
||||
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
|
||||
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
|
||||
if (!descriptor) {
|
||||
return { connection_type: 'UNKNOWN' };
|
||||
|
|
@ -160,22 +160,22 @@ function renderMapping(
|
|||
}
|
||||
|
||||
function renderLookerMapping(row: Awaited<ReturnType<LocalLookerRuntimeStore['listConnectionMappings']>>[number]): string {
|
||||
const target = row.kloConnectionId ?? '[unmapped]';
|
||||
const target = row.ktxConnectionId ?? '[unmapped]';
|
||||
const metadata = [row.lookerDialect, row.lookerHost, row.lookerDatabase].filter(Boolean).join(', ');
|
||||
return `${row.lookerConnectionName} -> ${target}${metadata ? ` (${metadata}, source: ${row.source})` : ` (source: ${row.source})`}`;
|
||||
}
|
||||
|
||||
export async function runKloConnectionMapping(
|
||||
args: KloConnectionMappingArgs,
|
||||
io: KloCliIo = process,
|
||||
deps: KloConnectionMappingDeps = {},
|
||||
export async function runKtxConnectionMapping(
|
||||
args: KtxConnectionMappingArgs,
|
||||
io: KtxCliIo = process,
|
||||
deps: KtxConnectionMappingDeps = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
const project = await loadKloProject({ projectDir: args.projectDir });
|
||||
await seedLocalMappingStateFromKloYaml(project, args.connectionId);
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
await seedLocalMappingStateFromKtxYaml(project, args.connectionId);
|
||||
if (isLookerConnection(project, args.connectionId)) {
|
||||
assertLookerConnection(project, args.connectionId);
|
||||
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
|
||||
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
|
||||
|
||||
if (args.command === 'list') {
|
||||
const rows = await store.listConnectionMappings(args.connectionId);
|
||||
|
|
@ -191,7 +191,7 @@ export async function runKloConnectionMapping(
|
|||
await store.upsertConnectionMapping({
|
||||
lookerConnectionId: args.connectionId,
|
||||
lookerConnectionName: args.key,
|
||||
kloConnectionId: args.value,
|
||||
ktxConnectionId: args.value,
|
||||
source: 'cli',
|
||||
});
|
||||
io.stdout.write(`Set connectionMappings.${args.key} = ${args.value}\n`);
|
||||
|
|
@ -219,13 +219,13 @@ export async function runKloConnectionMapping(
|
|||
}
|
||||
|
||||
if (args.command === 'validate') {
|
||||
const knownKloConnectionIds = new Set(Object.keys(project.config.connections));
|
||||
const knownKtxConnectionIds = new Set(Object.keys(project.config.connections));
|
||||
const knownConnectionTypes = new Map(
|
||||
Object.entries(project.config.connections).map(([id, _config]) => [id, targetPhysicalInfo(project, id).connection_type]),
|
||||
);
|
||||
const validation = validateLookerMappings({
|
||||
mappings: await store.readMappings(args.connectionId),
|
||||
knownKloConnectionIds,
|
||||
knownKtxConnectionIds,
|
||||
knownConnectionTypes,
|
||||
});
|
||||
if (!validation.ok) {
|
||||
|
|
@ -255,7 +255,7 @@ export async function runKloConnectionMapping(
|
|||
}
|
||||
|
||||
assertMetabaseConnection(project, args.connectionId);
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
|
||||
|
||||
if (args.command === 'list') {
|
||||
const rows = await store.listDatabaseMappings(args.connectionId);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
|
||||
import {
|
||||
type KloCliCommandContext,
|
||||
type KtxCliCommandContext,
|
||||
parseNonEmptyAssignmentOption,
|
||||
parsePositiveIntegerOption,
|
||||
parseSafeConnectionIdOption,
|
||||
resolveCommandProjectDir,
|
||||
} from '../cli-program.js';
|
||||
import {
|
||||
type KloConnectionMetabaseSetupArgs,
|
||||
type KtxConnectionMetabaseSetupArgs,
|
||||
type MetabaseSetupMappingAssignment,
|
||||
type MetabaseSetupSyncMode,
|
||||
runKloConnectionMetabaseSetup,
|
||||
runKtxConnectionMetabaseSetup,
|
||||
} from './connection-metabase-setup.js';
|
||||
|
||||
const SYNC_MODE_CHOICES = ['ALL', 'ONLY', 'EXCEPT'] as const satisfies readonly MetabaseSetupSyncMode[];
|
||||
|
|
@ -51,21 +51,21 @@ function collectMappingOption(
|
|||
}
|
||||
|
||||
async function runMetabaseSetupArgs(
|
||||
context: KloCliCommandContext,
|
||||
args: KloConnectionMetabaseSetupArgs,
|
||||
context: KtxCliCommandContext,
|
||||
args: KtxConnectionMetabaseSetupArgs,
|
||||
): Promise<void> {
|
||||
const runner = context.deps.connectionMetabaseSetup ?? runKloConnectionMetabaseSetup;
|
||||
const runner = context.deps.connectionMetabaseSetup ?? runKtxConnectionMetabaseSetup;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerConnectionMetabaseCommands(connection: Command, context: KloCliCommandContext): void {
|
||||
export function registerConnectionMetabaseCommands(connection: Command, context: KtxCliCommandContext): void {
|
||||
const metabase = connection
|
||||
.command('metabase')
|
||||
.description('Configure Metabase connections')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
metabase.action(() => {
|
||||
|
|
@ -76,7 +76,7 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
|
|||
metabase
|
||||
.command('setup')
|
||||
.description('Guided setup for a Metabase connection')
|
||||
.option('--id <connectionId>', 'KLO connection id to write', parseSafeConnectionIdOption)
|
||||
.option('--id <connectionId>', 'KTX connection id to write', parseSafeConnectionIdOption)
|
||||
.option('--url <url>', 'Metabase API URL')
|
||||
.addOption(new Option('--api-key <key>', 'Metabase API key').conflicts('mintApiKey'))
|
||||
.option('--mint-api-key', 'Mint a Metabase API key with credentials', false)
|
||||
|
|
@ -85,10 +85,10 @@ export function registerConnectionMetabaseCommands(connection: Command, context:
|
|||
.addHelpText(
|
||||
'after',
|
||||
'\nGuided equivalent of:\n' +
|
||||
' klo connection mapping refresh <connectionId> --auto-accept\n' +
|
||||
' klo connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
|
||||
' klo connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
|
||||
' klo ingest <connectionId>\n',
|
||||
' ktx connection mapping refresh <connectionId> --auto-accept\n' +
|
||||
' ktx connection mapping set <connectionId> databaseMappings <id>=<target>\n' +
|
||||
' ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true\n' +
|
||||
' ktx ingest <connectionId>\n',
|
||||
)
|
||||
.option(
|
||||
'--map <metabaseDatabaseId=targetConnectionId>',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { LocalMetabaseSourceStateReader } from '@klo/context/ingest';
|
||||
import { initKloProject, kloLocalStateDbPath, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
|
||||
import { LocalMetabaseSourceStateReader } from '@ktx/context/ingest';
|
||||
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { runKloConnectionMetabaseSetup } from './connection-metabase-setup.js';
|
||||
import { runKtxConnectionMetabaseSetup } from './connection-metabase-setup.js';
|
||||
|
||||
const CANCEL_PROMPT = Symbol('cancel');
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ function makeIo(options: { isTTY?: boolean; stdinIsTTY?: boolean } = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
describe('runKloConnectionMetabaseSetup', () => {
|
||||
describe('runKtxConnectionMetabaseSetup', () => {
|
||||
const fakeMetabaseCredential = 'mb_example';
|
||||
const existingMetabaseCredential = 'mb_existing';
|
||||
const fakeAdminCredential = 'pw';
|
||||
|
|
@ -144,9 +144,9 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
let projectDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-metabase-setup-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-metabase-setup-'));
|
||||
projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'metabase-setup' });
|
||||
await initKtxProject({ projectDir, projectName: 'metabase-setup' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -154,15 +154,15 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
});
|
||||
|
||||
async function writeConnections(connections: Record<string, { driver: string; [key: string]: unknown }>) {
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig({
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
connections,
|
||||
}),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Seed Metabase setup test connections',
|
||||
);
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -230,17 +230,17 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Connection: metabase');
|
||||
expect(io.stdout()).toContain('Discovered 1 database');
|
||||
expect(io.stdout()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
|
||||
expect(io.stdout()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
|
||||
expect(io.stdout()).not.toContain('mb_example');
|
||||
expect(io.stderr()).not.toContain('mb_example');
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('driver: metabase');
|
||||
expect(config).toContain('api_url: http://metabase.example.test:3000');
|
||||
expect(config).toContain('api_key: mb_example');
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{
|
||||
metabaseDatabaseId: 2,
|
||||
|
|
@ -275,7 +275,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -295,8 +295,8 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
|
||||
]);
|
||||
|
|
@ -314,7 +314,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -350,7 +350,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -370,8 +370,8 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
|
||||
]);
|
||||
|
|
@ -384,7 +384,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -412,7 +412,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -440,7 +440,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
|
||||
const missingUsernameIo = makeIo();
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -462,7 +462,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
|
||||
const missingPasswordIo = makeIo();
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -500,7 +500,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const mintingIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -537,7 +537,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
expect(mintingIo.stdout()).not.toContain(fakeAdminCredential);
|
||||
expect(mintingIo.stderr()).not.toContain(fakeAdminCredential);
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('driver: metabase');
|
||||
expect(config).toContain('api_url: http://metabase.example.test:3000');
|
||||
expect(config).toContain(`api_key: ${mintedMetabaseCredential}`);
|
||||
|
|
@ -548,7 +548,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -590,7 +590,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -640,7 +640,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -660,8 +660,8 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 1, targetConnectionId: 'orbit', syncEnabled: true },
|
||||
{ metabaseDatabaseId: 2, targetConnectionId: null, syncEnabled: false },
|
||||
|
|
@ -676,7 +676,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -712,7 +712,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -759,7 +759,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -782,12 +782,12 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('driver: metabase');
|
||||
expect(io.stderr()).toContain(`klo ingest metabase --project-dir ${projectDir}`);
|
||||
expect(io.stderr()).toContain(`ktx ingest metabase --project-dir ${projectDir}`);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit' },
|
||||
]);
|
||||
|
|
@ -810,7 +810,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -857,7 +857,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const interactiveMetabaseCredential = 'mb_interactive_fixture';
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -882,13 +882,13 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('driver: metabase');
|
||||
expect(config).toContain('api_url: http://metabase.example.test:3000');
|
||||
expect(config).toContain(`api_key: ${interactiveMetabaseCredential}`);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{
|
||||
metabaseDatabaseId: 2,
|
||||
|
|
@ -931,7 +931,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const events: string[] = [];
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -958,8 +958,8 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toMatchObject([
|
||||
{ metabaseDatabaseId: 2, targetConnectionId: 'orbit', syncEnabled: true },
|
||||
{ metabaseDatabaseId: 3, targetConnectionId: 'warehouse2', syncEnabled: false },
|
||||
|
|
@ -997,7 +997,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const events: string[] = [];
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -1023,7 +1023,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(events).toContain('intro:KLO Metabase setup');
|
||||
expect(events).toContain('intro:KTX Metabase setup');
|
||||
expect(events.some((event) => event.startsWith('spinner.start:Testing Metabase connection'))).toBe(true);
|
||||
expect(events.some((event) => event.startsWith('spinner.stop:Metabase reachable'))).toBe(true);
|
||||
expect(events.some((event) => event.startsWith('spinner.start:Discovering Metabase databases'))).toBe(true);
|
||||
|
|
@ -1053,7 +1053,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -1081,7 +1081,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const beforeConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const beforeConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
const metabaseClient = makeMetabaseClient({
|
||||
testConnectionSuccess: true,
|
||||
databases: [
|
||||
|
|
@ -1098,7 +1098,7 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
const cancelMetabaseCredential = 'mb_cancel_fixture';
|
||||
|
||||
await expect(
|
||||
runKloConnectionMetabaseSetup(
|
||||
runKtxConnectionMetabaseSetup(
|
||||
{
|
||||
command: 'setup',
|
||||
projectDir,
|
||||
|
|
@ -1126,11 +1126,11 @@ describe('runKloConnectionMetabaseSetup', () => {
|
|||
expect(io.stderr()).toContain('Setup cancelled.');
|
||||
expect(io.stderr()).not.toContain(cancelMetabaseCredential);
|
||||
|
||||
const afterConfig = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const afterConfig = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(afterConfig).toBe(beforeConfig);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
await expect(store.listDatabaseMappings('metabase')).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
select,
|
||||
text,
|
||||
} from '@clack/prompts';
|
||||
import { localConnectionToWarehouseDescriptor } from '@klo/context/connections';
|
||||
import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
|
||||
import {
|
||||
DEFAULT_METABASE_CLIENT_CONFIG,
|
||||
DefaultMetabaseConnectionClientFactory,
|
||||
|
|
@ -23,21 +23,21 @@ import {
|
|||
type MetabaseSyncMode,
|
||||
metabaseRuntimeConfigFromLocalConnection,
|
||||
validateMappingPhysicalMatch,
|
||||
} from '@klo/context/ingest';
|
||||
} from '@ktx/context/ingest';
|
||||
import {
|
||||
type KloLocalProject,
|
||||
type KloProjectConnectionConfig,
|
||||
kloLocalStateDbPath,
|
||||
loadKloProject,
|
||||
serializeKloProjectConfig,
|
||||
} from '@klo/context/project';
|
||||
type KtxLocalProject,
|
||||
type KtxProjectConnectionConfig,
|
||||
ktxLocalStateDbPath,
|
||||
loadKtxProject,
|
||||
serializeKtxProjectConfig,
|
||||
} from '@ktx/context/project';
|
||||
|
||||
import { createClackSpinner, type KloCliSpinner } from '../clack.js';
|
||||
import type { KloCliIo } from '../cli-runtime.js';
|
||||
import { createClackSpinner, type KtxCliSpinner } from '../clack.js';
|
||||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
import { withMenuOptionsSpacing, withMultiselectNavigation } from '../prompt-navigation.js';
|
||||
import { type KloPublicIngestArgs, runKloPublicIngest } from '../public-ingest.js';
|
||||
import { type KtxPublicIngestArgs, runKtxPublicIngest } from '../public-ingest.js';
|
||||
|
||||
export type KloMetabaseSetupInputMode = 'auto' | 'disabled';
|
||||
export type KtxMetabaseSetupInputMode = 'auto' | 'disabled';
|
||||
|
||||
export type MetabaseSetupSyncMode = MetabaseSyncMode;
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ export interface MetabaseSetupPromptAdapter {
|
|||
outro(message?: string): void;
|
||||
note(message: string, title: string): void;
|
||||
log: MetabaseSetupLogger;
|
||||
spinner(): KloCliSpinner;
|
||||
spinner(): KtxCliSpinner;
|
||||
select<T extends string>(options: { message: string; options: Array<MetabaseSetupPromptOption<T>> }): Promise<T>;
|
||||
multiselect<Value extends number | string>(options: {
|
||||
message: string;
|
||||
|
|
@ -71,7 +71,7 @@ export interface MetabaseSetupPromptAdapter {
|
|||
cancel(message: string): void;
|
||||
}
|
||||
|
||||
type KloMetabaseSetupInteractiveIo = KloCliIo & {
|
||||
type KtxMetabaseSetupInteractiveIo = KtxCliIo & {
|
||||
stdin?: { isTTY?: boolean };
|
||||
};
|
||||
|
||||
|
|
@ -86,9 +86,9 @@ export interface MintMetabaseApiKeyArgs {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KloCliIo) => Promise<string>;
|
||||
export type MintMetabaseApiKey = (args: MintMetabaseApiKeyArgs, io: KtxCliIo) => Promise<string>;
|
||||
|
||||
export interface KloConnectionMetabaseSetupArgs {
|
||||
export interface KtxConnectionMetabaseSetupArgs {
|
||||
command: 'setup';
|
||||
projectDir: string;
|
||||
connectionId?: string;
|
||||
|
|
@ -102,20 +102,20 @@ export interface KloConnectionMetabaseSetupArgs {
|
|||
syncMode: MetabaseSetupSyncMode;
|
||||
runIngest: boolean;
|
||||
yes: boolean;
|
||||
inputMode: KloMetabaseSetupInputMode;
|
||||
inputMode: KtxMetabaseSetupInputMode;
|
||||
}
|
||||
|
||||
export interface KloConnectionMetabaseSetupDeps {
|
||||
export interface KtxConnectionMetabaseSetupDeps {
|
||||
createMetabaseClient?: (
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
) => Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>>;
|
||||
mintMetabaseApiKey?: MintMetabaseApiKey;
|
||||
prompts?: MetabaseSetupPromptAdapter;
|
||||
runPublicIngest?: (args: Extract<KloPublicIngestArgs, { command: 'run' }>, io: KloCliIo) => Promise<number>;
|
||||
runPublicIngest?: (args: Extract<KtxPublicIngestArgs, { command: 'run' }>, io: KtxCliIo) => Promise<number>;
|
||||
}
|
||||
|
||||
function isMetabaseConnection(connection: KloProjectConnectionConfig | undefined): boolean {
|
||||
function isMetabaseConnection(connection: KtxProjectConnectionConfig | undefined): boolean {
|
||||
return (
|
||||
String(connection?.driver ?? '')
|
||||
.trim()
|
||||
|
|
@ -131,22 +131,22 @@ function uniqueSorted(values: number[]): number[] {
|
|||
return [...new Set(values)].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function resolveMetabaseUrl(connection: KloProjectConnectionConfig | undefined): string | undefined {
|
||||
function resolveMetabaseUrl(connection: KtxProjectConnectionConfig | undefined): string | undefined {
|
||||
return stringField(connection?.api_url) ?? stringField(connection?.apiUrl) ?? stringField(connection?.url);
|
||||
}
|
||||
|
||||
function resolveLiteralMetabaseApiKey(connection: KloProjectConnectionConfig | undefined): string | undefined {
|
||||
function resolveLiteralMetabaseApiKey(connection: KtxProjectConnectionConfig | undefined): string | undefined {
|
||||
return stringField(connection?.api_key) ?? stringField(connection?.apiKey);
|
||||
}
|
||||
|
||||
function listMetabaseConnectionIds(project: KloLocalProject): string[] {
|
||||
function listMetabaseConnectionIds(project: KtxLocalProject): string[] {
|
||||
return Object.entries(project.config.connections)
|
||||
.filter(([_connectionId, connection]) => isMetabaseConnection(connection))
|
||||
.map(([connectionId]) => connectionId)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function listWarehouseConnectionIds(project: KloLocalProject): string[] {
|
||||
function listWarehouseConnectionIds(project: KtxLocalProject): string[] {
|
||||
return Object.entries(project.config.connections)
|
||||
.filter(([connectionId, connection]) => localConnectionToWarehouseDescriptor(connectionId, connection) != null)
|
||||
.map(([connectionId]) => connectionId)
|
||||
|
|
@ -165,7 +165,7 @@ function redactSecrets(message: string, secrets: string[]): string {
|
|||
}
|
||||
|
||||
async function createDefaultMetabaseClient(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
): Promise<Pick<MetabaseRuntimeClient, 'testConnection' | 'getDatabases' | 'cleanup'>> {
|
||||
const factory = new DefaultMetabaseConnectionClientFactory(
|
||||
|
|
@ -192,7 +192,7 @@ async function defaultMintMetabaseApiKey(args: MintMetabaseApiKeyArgs): Promise<
|
|||
|
||||
const mintedKey = await sessionClient.createApiKey({
|
||||
groupId: adminGroup.id,
|
||||
name: `KLO CLI ${new Date().toISOString()}`,
|
||||
name: `KTX CLI ${new Date().toISOString()}`,
|
||||
});
|
||||
const trimmedKey = stringField(mintedKey);
|
||||
if (!trimmedKey) {
|
||||
|
|
@ -237,7 +237,7 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
|
|||
log.error(message);
|
||||
},
|
||||
},
|
||||
spinner(): KloCliSpinner {
|
||||
spinner(): KtxCliSpinner {
|
||||
return createClackSpinner();
|
||||
},
|
||||
async select<T extends string>(options: {
|
||||
|
|
@ -271,8 +271,8 @@ export function createClackMetabaseSetupPromptAdapter(): MetabaseSetupPromptAdap
|
|||
}
|
||||
|
||||
function isInteractiveMetabaseSetupIo(
|
||||
args: Pick<KloConnectionMetabaseSetupArgs, 'inputMode'>,
|
||||
io: KloMetabaseSetupInteractiveIo,
|
||||
args: Pick<KtxConnectionMetabaseSetupArgs, 'inputMode'>,
|
||||
io: KtxMetabaseSetupInteractiveIo,
|
||||
): boolean {
|
||||
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ function normalizeDiscoveredDatabases(databases: MetabaseDatabase[]): Array<{
|
|||
}));
|
||||
}
|
||||
|
||||
function targetPhysicalInfo(project: KloLocalProject, connectionId: string) {
|
||||
function targetPhysicalInfo(project: KtxLocalProject, connectionId: string) {
|
||||
const descriptor = localConnectionToWarehouseDescriptor(connectionId, project.config.connections[connectionId]);
|
||||
if (!descriptor) {
|
||||
return { connection_type: 'UNKNOWN' };
|
||||
|
|
@ -338,23 +338,23 @@ function noteMetabaseSetupSummary(options: {
|
|||
);
|
||||
}
|
||||
|
||||
export async function runKloConnectionMetabaseSetup(
|
||||
args: KloConnectionMetabaseSetupArgs,
|
||||
io: KloCliIo,
|
||||
deps: KloConnectionMetabaseSetupDeps = {},
|
||||
export async function runKtxConnectionMetabaseSetup(
|
||||
args: KtxConnectionMetabaseSetupArgs,
|
||||
io: KtxCliIo,
|
||||
deps: KtxConnectionMetabaseSetupDeps = {},
|
||||
): Promise<number> {
|
||||
let apiKeyForRedaction = args.apiKey;
|
||||
let passwordForRedaction = args.metabasePassword;
|
||||
const interactiveIo = io as KloMetabaseSetupInteractiveIo;
|
||||
const interactiveIo = io as KtxMetabaseSetupInteractiveIo;
|
||||
const isInteractive = isInteractiveMetabaseSetupIo(args, interactiveIo);
|
||||
const prompts = deps.prompts ?? (isInteractive ? createClackMetabaseSetupPromptAdapter() : undefined);
|
||||
|
||||
try {
|
||||
if (isInteractive && prompts) {
|
||||
prompts.intro('KLO Metabase setup');
|
||||
prompts.intro('KTX Metabase setup');
|
||||
}
|
||||
|
||||
const project = await loadKloProject({ projectDir: args.projectDir });
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const existingMetabaseConnectionIds = listMetabaseConnectionIds(project);
|
||||
let connectionId: string;
|
||||
|
||||
|
|
@ -491,7 +491,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
throw new Error('Metabase API key is required (use --api-key)');
|
||||
}
|
||||
|
||||
const transientConnectionConfig: KloProjectConnectionConfig = {
|
||||
const transientConnectionConfig: KtxProjectConnectionConfig = {
|
||||
...(existingConnection ?? {}),
|
||||
driver: 'metabase',
|
||||
api_url: url,
|
||||
|
|
@ -504,7 +504,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
[connectionId]: transientConnectionConfig,
|
||||
},
|
||||
};
|
||||
const discoveryProject: KloLocalProject = { ...project, config: configWithTransient };
|
||||
const discoveryProject: KtxLocalProject = { ...project, config: configWithTransient };
|
||||
|
||||
for (const mapping of args.mappings) {
|
||||
if (!configWithTransient.connections[mapping.targetConnectionId]) {
|
||||
|
|
@ -618,7 +618,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
}
|
||||
|
||||
const targetConnectionId = await prompts.select({
|
||||
message: `Map Metabase database ${database.id} ("${database.name}") to which KLO connection?`,
|
||||
message: `Map Metabase database ${database.id} ("${database.name}") to which KTX connection?`,
|
||||
options: warehouseConnectionIds.map((warehouseId) => ({ value: warehouseId, label: warehouseId })),
|
||||
});
|
||||
resolvedMappings.push({ metabaseDatabaseId: databaseId, targetConnectionId });
|
||||
|
|
@ -641,7 +641,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
syncEnabledDatabaseIds: resolvedSyncEnabledDatabaseIds,
|
||||
});
|
||||
const confirmed = await prompts.confirm({
|
||||
message: 'Write changes to klo.yaml and enable sync?',
|
||||
message: 'Write changes to ktx.yaml and enable sync?',
|
||||
initialValue: true,
|
||||
});
|
||||
if (!confirmed) {
|
||||
|
|
@ -675,15 +675,15 @@ export async function runKloConnectionMetabaseSetup(
|
|||
}
|
||||
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig(configWithTransient),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig(configWithTransient),
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
`Setup Metabase connection ${connectionId}`,
|
||||
);
|
||||
|
||||
const updatedProject = await loadKloProject({ projectDir: args.projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(updatedProject) });
|
||||
const updatedProject = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(updatedProject) });
|
||||
|
||||
await store.refreshDiscoveredDatabases({ connectionId, discovered });
|
||||
|
||||
|
|
@ -716,7 +716,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
const unhydrated = await store.getUnhydratedSyncEnabledMappingIds(connectionId);
|
||||
if (unhydrated.length > 0) {
|
||||
io.stderr.write(
|
||||
`Sync-enabled mappings are missing discovery metadata; run klo connection mapping refresh ${connectionId} --auto-accept\n`,
|
||||
`Sync-enabled mappings are missing discovery metadata; run ktx connection mapping refresh ${connectionId} --auto-accept\n`,
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -743,10 +743,10 @@ export async function runKloConnectionMetabaseSetup(
|
|||
|
||||
io.stdout.write(`Connection: ${connectionId}\n`);
|
||||
io.stdout.write(`Discovered ${discovered.length} ${discovered.length === 1 ? 'database' : 'databases'}\n`);
|
||||
io.stdout.write(`Next: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
|
||||
io.stdout.write(`Next: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
|
||||
|
||||
if (args.runIngest) {
|
||||
const ingestRunner = deps.runPublicIngest ?? runKloPublicIngest;
|
||||
const ingestRunner = deps.runPublicIngest ?? runKtxPublicIngest;
|
||||
const exitCode = await ingestRunner(
|
||||
{
|
||||
command: 'run',
|
||||
|
|
@ -759,7 +759,7 @@ export async function runKloConnectionMetabaseSetup(
|
|||
io,
|
||||
);
|
||||
if (exitCode !== 0) {
|
||||
io.stderr.write(`Ingest failed; re-run: klo ingest ${connectionId} --project-dir ${args.projectDir}\n`);
|
||||
io.stderr.write(`Ingest failed; re-run: ktx ingest ${connectionId} --project-dir ${args.projectDir}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
|
||||
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloConnectionNotionArgs } from './connection-notion.js';
|
||||
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxConnectionNotionArgs } from './connection-notion.js';
|
||||
|
||||
interface NotionPickOptions {
|
||||
input?: boolean;
|
||||
|
|
@ -36,7 +36,7 @@ function normalizeNotionPageId(value: string): string {
|
|||
return `${lower.slice(0, 8)}-${lower.slice(8, 12)}-${lower.slice(12, 16)}-${lower.slice(16, 20)}-${lower.slice(20)}`;
|
||||
}
|
||||
|
||||
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KloConnectionNotionArgs {
|
||||
function buildPickArgs(connectionId: string, projectDir: string, options: NotionPickOptions): KtxConnectionNotionArgs {
|
||||
if (options.input !== false) {
|
||||
return {
|
||||
command: 'pick',
|
||||
|
|
@ -59,19 +59,19 @@ function buildPickArgs(connectionId: string, projectDir: string, options: Notion
|
|||
};
|
||||
}
|
||||
|
||||
async function runConnectionNotionArgs(context: KloCliCommandContext, args: KloConnectionNotionArgs): Promise<void> {
|
||||
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKloConnectionNotion;
|
||||
async function runConnectionNotionArgs(context: KtxCliCommandContext, args: KtxConnectionNotionArgs): Promise<void> {
|
||||
const runner = context.deps.connectionNotion ?? (await import('./connection-notion.js')).runKtxConnectionNotion;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerConnectionNotionCommands(connect: Command, context: KloCliCommandContext): void {
|
||||
export function registerConnectionNotionCommands(connect: Command, context: KtxCliCommandContext): void {
|
||||
const notion = connect
|
||||
.command('notion')
|
||||
.description('Configure Notion source selection')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
notion.action(() => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
type PickerCommand,
|
||||
type PickerState,
|
||||
} from './connection-notion-tree.js';
|
||||
import type { KloCliIo } from '../index.js';
|
||||
import type { KtxCliIo } from '../index.js';
|
||||
|
||||
const COLOR_THEME = {
|
||||
text: 'white',
|
||||
|
|
@ -28,9 +28,9 @@ const NO_COLOR_THEME = {
|
|||
|
||||
type NotionPickerTheme = Record<keyof typeof COLOR_THEME, string>;
|
||||
|
||||
export interface NotionPickerTuiIo extends KloCliIo {
|
||||
export interface NotionPickerTuiIo extends KtxCliIo {
|
||||
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
|
||||
stdout: KloCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
|
||||
stdout: KtxCliIo['stdout'] & { isTTY?: boolean; columns?: number; rows?: number };
|
||||
}
|
||||
|
||||
interface InkKey {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
initKloProject,
|
||||
loadKloProject,
|
||||
serializeKloProjectConfig,
|
||||
type KloProjectConfig,
|
||||
} from '@klo/context/project';
|
||||
initKtxProject,
|
||||
loadKtxProject,
|
||||
serializeKtxProjectConfig,
|
||||
type KtxProjectConfig,
|
||||
} from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
applyNotionPickerWriteback,
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
notionPickerPageFromSearchResult,
|
||||
normalizeNotionPageId,
|
||||
resolveNotionWorkspaceLabel,
|
||||
runKloConnectionNotion,
|
||||
runKtxConnectionNotion,
|
||||
type NotionPickerApi,
|
||||
type PickerRenderInput,
|
||||
type PickerRenderResult,
|
||||
|
|
@ -91,24 +91,24 @@ describe('normalizeNotionPageId', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('runKloConnectionNotion', () => {
|
||||
describe('runKtxConnectionNotion', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-notion-pick-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-notion-pick-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function writeProjectConfig(projectDir: string, config: KloProjectConfig): Promise<void> {
|
||||
const project = await loadKloProject({ projectDir });
|
||||
async function writeProjectConfig(projectDir: string, config: KtxProjectConfig): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig(config),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig(config),
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'seed test config',
|
||||
);
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ describe('runKloConnectionNotion', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloConnectionNotion(
|
||||
runKtxConnectionNotion(
|
||||
{
|
||||
command: 'pick',
|
||||
projectDir: '/tmp/project',
|
||||
|
|
@ -138,7 +138,7 @@ describe('runKloConnectionNotion', () => {
|
|||
|
||||
it('writes selected root_page_ids while preserving every other Notion connection field', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeProjectConfig(projectDir, {
|
||||
...initialized.config,
|
||||
connections: {
|
||||
|
|
@ -160,7 +160,7 @@ describe('runKloConnectionNotion', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionNotion(
|
||||
runKtxConnectionNotion(
|
||||
{
|
||||
command: 'pick',
|
||||
projectDir,
|
||||
|
|
@ -175,7 +175,7 @@ describe('runKloConnectionNotion', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(yaml).toContain('crawl_mode: selected_roots');
|
||||
expect(yaml).toContain('root_page_ids:');
|
||||
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
|
||||
|
|
@ -193,7 +193,7 @@ describe('runKloConnectionNotion', () => {
|
|||
|
||||
it('rejects empty writeback, missing connections, and non-Notion connections', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeProjectConfig(projectDir, {
|
||||
...initialized.config,
|
||||
connections: {
|
||||
|
|
@ -204,7 +204,7 @@ describe('runKloConnectionNotion', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
|
||||
await expect(applyNotionPickerWriteback(project, 'warehouse', [])).rejects.toThrow(
|
||||
'connection notion pick requires at least one root page id',
|
||||
|
|
@ -297,7 +297,7 @@ describe('runKloConnectionNotion', () => {
|
|||
|
||||
it('runs interactive discovery, warns about stale roots, renders the TUI, and saves selected roots', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeProjectConfig(projectDir, {
|
||||
...initialized.config,
|
||||
connections: {
|
||||
|
|
@ -330,7 +330,7 @@ describe('runKloConnectionNotion', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionNotion(
|
||||
runKtxConnectionNotion(
|
||||
{
|
||||
command: 'pick',
|
||||
projectDir,
|
||||
|
|
@ -346,7 +346,7 @@ describe('runKloConnectionNotion', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(yaml).toContain('crawl_mode: selected_roots');
|
||||
expect(yaml).toContain(PAGE_IDS.engineering);
|
||||
expect(yaml).not.toContain(PAGE_IDS.stale);
|
||||
|
|
@ -357,7 +357,7 @@ describe('runKloConnectionNotion', () => {
|
|||
|
||||
it('passes partial-discovery warnings into the TUI banner state', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeProjectConfig(projectDir, {
|
||||
...initialized.config,
|
||||
connections: {
|
||||
|
|
@ -394,7 +394,7 @@ describe('runKloConnectionNotion', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionNotion(
|
||||
runKtxConnectionNotion(
|
||||
{
|
||||
command: 'pick',
|
||||
projectDir,
|
||||
|
|
@ -422,7 +422,7 @@ describe('runKloConnectionNotion', () => {
|
|||
|
||||
it('quits interactive mode without writing when the TUI returns quit', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const initialized = await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
const initialized = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeProjectConfig(projectDir, {
|
||||
...initialized.config,
|
||||
connections: {
|
||||
|
|
@ -440,11 +440,11 @@ describe('runKloConnectionNotion', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
const before = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const before = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnectionNotion(
|
||||
runKtxConnectionNotion(
|
||||
{
|
||||
command: 'pick',
|
||||
projectDir,
|
||||
|
|
@ -460,7 +460,7 @@ describe('runKloConnectionNotion', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toBe(before);
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(before);
|
||||
expect(io.stdout()).toContain('No changes saved.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@klo/context/connections';
|
||||
import { type NotionApi, type NotionBotInfo, NotionClient } from '@klo/context/ingest';
|
||||
import { parseNotionConnectionConfig, resolveNotionAuthToken } from '@ktx/context/connections';
|
||||
import { type NotionApi, type NotionBotInfo, NotionClient } from '@ktx/context/ingest';
|
||||
import {
|
||||
type KloLocalProject,
|
||||
type KloProjectConnectionConfig,
|
||||
loadKloProject,
|
||||
serializeKloProjectConfig,
|
||||
} from '@klo/context/project';
|
||||
import type { KloCliIo } from '../index.js';
|
||||
type KtxLocalProject,
|
||||
type KtxProjectConnectionConfig,
|
||||
loadKtxProject,
|
||||
serializeKtxProjectConfig,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from '../index.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
import { buildInitialState, buildPickerTree, type NotionPickerPageInput } from './connection-notion-tree.js';
|
||||
import {
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
profileMark('module:commands/connection-notion');
|
||||
|
||||
export type KloConnectionNotionArgs =
|
||||
export type KtxConnectionNotionArgs =
|
||||
| {
|
||||
command: 'pick';
|
||||
projectDir: string;
|
||||
|
|
@ -36,9 +36,9 @@ export type KloConnectionNotionArgs =
|
|||
export type NotionPickerApi = Pick<NotionApi, 'search' | 'retrieveBotUser'>;
|
||||
export type { PickerRenderInput, PickerRenderResult };
|
||||
|
||||
interface KloConnectionNotionDeps {
|
||||
interface KtxConnectionNotionDeps {
|
||||
env?: Record<string, string | undefined>;
|
||||
loadProject?: typeof loadKloProject;
|
||||
loadProject?: typeof loadKtxProject;
|
||||
createNotionApi?: (authToken: string) => NotionPickerApi;
|
||||
renderPicker?: (input: PickerRenderInput, io: NotionPickerTuiIo) => Promise<PickerRenderResult>;
|
||||
}
|
||||
|
|
@ -168,7 +168,7 @@ export async function resolveNotionWorkspaceLabel(api: NotionPickerApi, connecti
|
|||
}
|
||||
}
|
||||
|
||||
function notionConnection(project: KloLocalProject, connectionId: string): KloProjectConnectionConfig {
|
||||
function notionConnection(project: KtxLocalProject, connectionId: string): KtxProjectConnectionConfig {
|
||||
const connection = project.config.connections[connectionId];
|
||||
if (!connection) {
|
||||
throw new Error(`Connection "${connectionId}" not found`);
|
||||
|
|
@ -180,7 +180,7 @@ function notionConnection(project: KloLocalProject, connectionId: string): KloPr
|
|||
}
|
||||
|
||||
export async function applyNotionPickerWriteback(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
rootPageIds: string[],
|
||||
): Promise<void> {
|
||||
|
|
@ -202,22 +202,22 @@ export async function applyNotionPickerWriteback(
|
|||
};
|
||||
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig(nextConfig),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig(nextConfig),
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
`Pick Notion roots: ${connectionId} (${rootPageIds.length} pages)`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runKloConnectionNotion(
|
||||
args: KloConnectionNotionArgs,
|
||||
io: KloCliIo = process,
|
||||
deps: KloConnectionNotionDeps = {},
|
||||
export async function runKtxConnectionNotion(
|
||||
args: KtxConnectionNotionArgs,
|
||||
io: KtxCliIo = process,
|
||||
deps: KtxConnectionNotionDeps = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
assertSafeConnectionId(args.connectionId);
|
||||
const loadProject = deps.loadProject ?? loadKloProject;
|
||||
const loadProject = deps.loadProject ?? loadKtxProject;
|
||||
|
||||
if (args.mode === 'interactive') {
|
||||
const project = await loadProject({ projectDir: args.projectDir });
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
import {
|
||||
type CommandWithGlobalOptions,
|
||||
type KloCliCommandContext,
|
||||
type KtxCliCommandContext,
|
||||
resolveCommandProjectDirOverride,
|
||||
} from '../cli-program.js';
|
||||
import {
|
||||
type KloDemoArgs,
|
||||
type KloDemoInputMode,
|
||||
type KloDemoMode,
|
||||
type KloDemoOutputMode,
|
||||
type KtxDemoArgs,
|
||||
type KtxDemoInputMode,
|
||||
type KtxDemoMode,
|
||||
type KtxDemoOutputMode,
|
||||
} from '../demo.js';
|
||||
import { defaultDemoProjectDir } from '../demo-assets.js';
|
||||
import { resolveProjectDir } from '../project-dir.js';
|
||||
|
|
@ -23,7 +23,7 @@ interface DemoOptions {
|
|||
projectDir?: string;
|
||||
}
|
||||
|
||||
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
|
||||
function demoOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
|
|
@ -37,14 +37,14 @@ function demoDoctorOutputMode(options: { json?: boolean }): 'plain' | 'json' {
|
|||
return options.json === true ? 'json' : 'plain';
|
||||
}
|
||||
|
||||
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KloDemoOutputMode {
|
||||
function demoInspectOutputMode(options: { plain?: boolean; json?: boolean }): KtxDemoOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
return 'plain';
|
||||
}
|
||||
|
||||
function demoInputMode(options: { input?: boolean }): { inputMode?: KloDemoInputMode } {
|
||||
function demoInputMode(options: { input?: boolean }): { inputMode?: KtxDemoInputMode } {
|
||||
return options.input === false ? { inputMode: 'disabled' } : {};
|
||||
}
|
||||
|
||||
|
|
@ -118,19 +118,19 @@ export function resolveDemoCommandOptions<T>(command: { opts: () => T; optsWithG
|
|||
return { ...inherited, ...definedOptions(command.opts() as Record<string, unknown>, inherited, command) } as T;
|
||||
}
|
||||
|
||||
async function runDemoArgs(context: KloCliCommandContext, args: KloDemoArgs): Promise<void> {
|
||||
const runner = context.deps.demo ?? (await import('../demo.js')).runKloDemo;
|
||||
async function runDemoArgs(context: KtxCliCommandContext, args: KtxDemoArgs): Promise<void> {
|
||||
const runner = context.deps.demo ?? (await import('../demo.js')).runKtxDemo;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerDemoCommands(
|
||||
program: Command,
|
||||
context: KloCliCommandContext,
|
||||
context: KtxCliCommandContext,
|
||||
options: { description?: string } = {},
|
||||
): void {
|
||||
const demo = program
|
||||
.command('demo')
|
||||
.description(options.description ?? 'Run the pre-seeded KLO demo or a full LLM-backed demo')
|
||||
.description(options.description ?? 'Run the pre-seeded KTX demo or a full LLM-backed demo')
|
||||
.addOption(
|
||||
new Option('--mode <mode>', 'Demo mode: seeded (default), replay, or full')
|
||||
.choices(['seeded', 'replay', 'full'])
|
||||
|
|
@ -260,7 +260,7 @@ export function registerDemoCommands(
|
|||
.addOption(new Option('--plain', 'Print plain text output instead of the visual demo').conflicts('json'))
|
||||
.addOption(new Option('--json', 'Print JSON output').conflicts('plain'))
|
||||
.option('--no-input', 'Disable interactive terminal input')
|
||||
.action(async (_options, command: { opts: () => { mode: KloDemoMode } & DemoOptions }) => {
|
||||
.action(async (_options, command: { opts: () => { mode: KtxDemoMode } & DemoOptions }) => {
|
||||
const options = resolveDemoCommandOptions(command);
|
||||
await runDemoArgs(context, {
|
||||
command: 'ingest',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Command } from '@commander-js/extra-typings';
|
||||
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloDoctorArgs } from '../doctor.js';
|
||||
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxDoctorArgs } from '../doctor.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/doctor-commands');
|
||||
|
|
@ -13,15 +13,15 @@ function inputMode(options: { input?: boolean }): { inputMode?: 'disabled' } {
|
|||
return options.input === false ? { inputMode: 'disabled' } : {};
|
||||
}
|
||||
|
||||
async function runDoctorArgs(context: KloCliCommandContext, args: KloDoctorArgs): Promise<void> {
|
||||
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKloDoctor;
|
||||
async function runDoctorArgs(context: KtxCliCommandContext, args: KtxDoctorArgs): Promise<void> {
|
||||
const runner = context.deps.doctor ?? (await import('../doctor.js')).runKtxDoctor;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerDoctorCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerDoctorCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const doctor = program
|
||||
.command('doctor')
|
||||
.description('Check KLO setup, project, and demo readiness')
|
||||
.description('Check KTX setup, project, and demo readiness')
|
||||
.option('--json', 'Print JSON output', false)
|
||||
.option('--no-input', 'Disable interactive terminal input')
|
||||
.action(async (options: { json?: boolean; input?: boolean }, command) => {
|
||||
|
|
@ -35,7 +35,7 @@ export function registerDoctorCommands(program: Command, context: KloCliCommandC
|
|||
|
||||
doctor
|
||||
.command('setup')
|
||||
.description('Check KLO install, build, and local runtime readiness')
|
||||
.description('Check KTX install, build, and local runtime readiness')
|
||||
.option('--json', 'Print JSON output', false)
|
||||
.option('--no-input', 'Disable interactive terminal input')
|
||||
.action(
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { resolve } from 'node:path';
|
||||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
import { type KloCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloCliDeps, KloCliIo } from '../index.js';
|
||||
import type { KloIngestArgs, KloIngestOutputMode } from '../ingest.js';
|
||||
import { type KtxCliCommandContext, type OutputModeOptions, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxCliDeps, KtxCliIo } from '../index.js';
|
||||
import type { KtxIngestArgs, KtxIngestOutputMode } from '../ingest.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/ingest-commands');
|
||||
|
||||
interface IngestCommandOptions {
|
||||
runIngestWithProgress: (
|
||||
args: KloIngestArgs,
|
||||
io: KloCliIo,
|
||||
deps: KloCliDeps,
|
||||
defaultRunIngest: (args: KloIngestArgs, io: KloCliIo) => Promise<number>,
|
||||
args: KtxIngestArgs,
|
||||
io: KtxCliIo,
|
||||
deps: KtxCliDeps,
|
||||
defaultRunIngest: (args: KtxIngestArgs, io: KtxCliIo) => Promise<number>,
|
||||
) => Promise<number>;
|
||||
}
|
||||
|
||||
function outputMode(options: OutputModeOptions): KloIngestOutputMode {
|
||||
function outputMode(options: OutputModeOptions): KtxIngestOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ function outputMode(options: OutputModeOptions): KloIngestOutputMode {
|
|||
return 'plain';
|
||||
}
|
||||
|
||||
function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
|
||||
function watchOutputMode(options: OutputModeOptions): KtxIngestOutputMode {
|
||||
if (options.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
|
|
@ -36,22 +36,22 @@ function watchOutputMode(options: OutputModeOptions): KloIngestOutputMode {
|
|||
return 'viz';
|
||||
}
|
||||
|
||||
function inputMode(options: OutputModeOptions): Pick<KloIngestArgs, 'inputMode'> {
|
||||
function inputMode(options: OutputModeOptions): Pick<KtxIngestArgs, 'inputMode'> {
|
||||
return options.input === false ? { inputMode: 'disabled' } : {};
|
||||
}
|
||||
|
||||
async function runIngestArgs(
|
||||
context: KloCliCommandContext,
|
||||
args: KloIngestArgs,
|
||||
context: KtxCliCommandContext,
|
||||
args: KtxIngestArgs,
|
||||
options: IngestCommandOptions,
|
||||
): Promise<void> {
|
||||
const { runKloIngest } = await import('../ingest.js');
|
||||
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKloIngest));
|
||||
const { runKtxIngest } = await import('../ingest.js');
|
||||
context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKtxIngest));
|
||||
}
|
||||
|
||||
export function registerIngestCommands(
|
||||
program: Command,
|
||||
context: KloCliCommandContext,
|
||||
context: KtxCliCommandContext,
|
||||
commandOptions: IngestCommandOptions,
|
||||
): void {
|
||||
const ingest = program
|
||||
|
|
@ -66,7 +66,7 @@ export function registerIngestCommands(
|
|||
ingest
|
||||
.command('run')
|
||||
.description('Run local ingest for one configured connection and source adapter')
|
||||
.requiredOption('--connection-id <connectionId>', 'KLO connection id')
|
||||
.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')
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { type Command, Option } from '@commander-js/extra-typings';
|
||||
import { collectOption, type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import { collectOption, type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import { wikiWriteCommandSchema } from '../command-schemas.js';
|
||||
import type { KloKnowledgeArgs } from '../knowledge.js';
|
||||
import type { KtxKnowledgeArgs } from '../knowledge.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/knowledge-commands');
|
||||
|
||||
async function runKnowledgeArgs(context: KloCliCommandContext, args: KloKnowledgeArgs): Promise<void> {
|
||||
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKloKnowledge;
|
||||
async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledgeArgs): Promise<void> {
|
||||
const runner = context.deps.knowledge ?? (await import('../knowledge.js')).runKtxKnowledge;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerWikiCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const wiki = program
|
||||
.command('wiki')
|
||||
.description('List, read, search, or write local wiki pages')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
wiki
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { InvalidArgumentError, type Command } from '@commander-js/extra-typings';
|
||||
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import { publicIngestReadCommandSchema, publicIngestRunCommandSchema } from '../command-schemas.js';
|
||||
import type { KloPublicIngestArgs, KloPublicIngestInputMode } from '../public-ingest.js';
|
||||
import type { KtxPublicIngestArgs, KtxPublicIngestInputMode } from '../public-ingest.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/public-ingest-commands');
|
||||
|
|
@ -12,26 +12,26 @@ interface PublicIngestOptions {
|
|||
input?: boolean;
|
||||
}
|
||||
|
||||
function inputMode(options: { input?: boolean }): KloPublicIngestInputMode {
|
||||
function inputMode(options: { input?: boolean }): KtxPublicIngestInputMode {
|
||||
return options.input === false ? 'disabled' : 'auto';
|
||||
}
|
||||
|
||||
async function runPublicIngestArgs(context: KloCliCommandContext, args: KloPublicIngestArgs): Promise<void> {
|
||||
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKloPublicIngest;
|
||||
async function runPublicIngestArgs(context: KtxCliCommandContext, args: KtxPublicIngestArgs): Promise<void> {
|
||||
const runner = context.deps.publicIngest ?? (await import('../public-ingest.js')).runKtxPublicIngest;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
function parsePublicIngestConnectionId(value: string): string {
|
||||
if (value === 'run') {
|
||||
throw new InvalidArgumentError('run is reserved; use klo dev ingest run for low-level adapter syntax');
|
||||
throw new InvalidArgumentError('run is reserved; use ktx dev ingest run for low-level adapter syntax');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function registerPublicIngestCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerPublicIngestCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const ingest = program
|
||||
.command('ingest')
|
||||
.description('Build and refresh KLO context from configured sources')
|
||||
.description('Build and refresh KTX context from configured sources')
|
||||
.usage('[options] [connectionId]')
|
||||
.argument('[connectionId]', 'Connection id to ingest', parsePublicIngestConnectionId)
|
||||
.option('--all', 'Ingest every eligible configured source', false)
|
||||
|
|
@ -42,12 +42,12 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
|
|||
[
|
||||
'',
|
||||
'Examples:',
|
||||
' klo ingest <connectionId> [options]',
|
||||
' klo ingest --all [options]',
|
||||
' klo ingest status [runId] [options]',
|
||||
' klo ingest watch [runId] [options]',
|
||||
' ktx ingest <connectionId> [options]',
|
||||
' ktx ingest --all [options]',
|
||||
' ktx ingest status [runId] [options]',
|
||||
' ktx ingest watch [runId] [options]',
|
||||
'',
|
||||
'Project directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.',
|
||||
'Project directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.',
|
||||
'',
|
||||
].join('\n'),
|
||||
)
|
||||
|
|
@ -58,7 +58,7 @@ export function registerPublicIngestCommands(program: Command, context: KloCliCo
|
|||
.action(async (connectionId: string | undefined, _options: PublicIngestOptions, command) => {
|
||||
const options = command.opts();
|
||||
if (options.all === true && connectionId) {
|
||||
throw new Error('klo ingest accepts either --all or <connectionId>, not both');
|
||||
throw new Error('ktx ingest accepts either --all or <connectionId>, not both');
|
||||
}
|
||||
const args = publicIngestRunCommandSchema.parse({
|
||||
command: 'run',
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
||||
import { type KloCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloScanArgs } from '../scan.js';
|
||||
import { type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxScanArgs } from '../scan.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/scan-commands');
|
||||
|
||||
async function runScanArgs(context: KloCliCommandContext, args: KloScanArgs): Promise<void> {
|
||||
const runner = context.deps.scan ?? (await import('../scan.js')).runKloScan;
|
||||
async function runScanArgs(context: KtxCliCommandContext, args: KtxScanArgs): Promise<void> {
|
||||
const runner = context.deps.scan ?? (await import('../scan.js')).runKtxScan;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
type KloScanModeOption = Extract<KloScanArgs, { command: 'run' }>['mode'];
|
||||
type KtxScanModeOption = Extract<KtxScanArgs, { command: 'run' }>['mode'];
|
||||
|
||||
function parseScanModeOption(value: string): KloScanModeOption {
|
||||
function parseScanModeOption(value: string): KtxScanModeOption {
|
||||
if (value === 'structural' || value === 'enriched' || value === 'relationships') {
|
||||
return value;
|
||||
}
|
||||
throw new InvalidArgumentError('Allowed choices are structural, enriched, relationships');
|
||||
}
|
||||
|
||||
type KloRelationshipStatusOption = Extract<KloScanArgs, { command: 'relationships' }>['status'];
|
||||
type KloRelationshipFeedbackDecisionOption = Extract<KloScanArgs, { command: 'relationshipFeedback' }>['decision'];
|
||||
type KtxRelationshipStatusOption = Extract<KtxScanArgs, { command: 'relationships' }>['status'];
|
||||
type KtxRelationshipFeedbackDecisionOption = Extract<KtxScanArgs, { command: 'relationshipFeedback' }>['decision'];
|
||||
|
||||
function parseRelationshipStatusOption(value: string): KloRelationshipStatusOption {
|
||||
function parseRelationshipStatusOption(value: string): KtxRelationshipStatusOption {
|
||||
if (value === 'accepted' || value === 'review' || value === 'rejected' || value === 'skipped' || value === 'all') {
|
||||
return value;
|
||||
}
|
||||
throw new InvalidArgumentError('Allowed choices are accepted, review, rejected, skipped, all');
|
||||
}
|
||||
|
||||
function parseRelationshipFeedbackDecisionOption(value: string): KloRelationshipFeedbackDecisionOption {
|
||||
function parseRelationshipFeedbackDecisionOption(value: string): KtxRelationshipFeedbackDecisionOption {
|
||||
if (value === 'accepted' || value === 'rejected' || value === 'all') {
|
||||
return value;
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ function relationshipDecisionArgs(options: {
|
|||
note?: string;
|
||||
json?: boolean;
|
||||
}): Pick<
|
||||
Extract<KloScanArgs, { command: 'relationshipDecision' }>,
|
||||
Extract<KtxScanArgs, { command: 'relationshipDecision' }>,
|
||||
'candidateId' | 'decision' | 'reviewer' | 'note' | 'json'
|
||||
> | null {
|
||||
const decisionCount = [options.accept !== undefined, options.reject !== undefined].filter(Boolean).length;
|
||||
|
|
@ -69,7 +69,7 @@ function relationshipDecisionArgs(options: {
|
|||
return {
|
||||
candidateId: options.accept,
|
||||
decision: 'accepted',
|
||||
reviewer: options.reviewer ?? 'klo',
|
||||
reviewer: options.reviewer ?? 'ktx',
|
||||
note: options.note ?? null,
|
||||
json: options.json === true,
|
||||
};
|
||||
|
|
@ -78,7 +78,7 @@ function relationshipDecisionArgs(options: {
|
|||
return {
|
||||
candidateId: options.reject,
|
||||
decision: 'rejected',
|
||||
reviewer: options.reviewer ?? 'klo',
|
||||
reviewer: options.reviewer ?? 'ktx',
|
||||
note: options.note ?? null,
|
||||
json: options.json === true,
|
||||
};
|
||||
|
|
@ -90,11 +90,11 @@ function collectRelationshipCandidateOption(value: string, previous: string[]):
|
|||
return [...previous, parseNonEmptyOption(value)];
|
||||
}
|
||||
|
||||
export function registerScanCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerScanCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const scan = program
|
||||
.command('scan')
|
||||
.description('Run or inspect standalone connection scans')
|
||||
.argument('[connectionId]', 'KLO connection id to scan')
|
||||
.argument('[connectionId]', 'KTX connection id to scan')
|
||||
.option(
|
||||
'--mode <mode>',
|
||||
'Scan mode: structural, enriched, relationships (default: structural)',
|
||||
|
|
@ -105,7 +105,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
)
|
||||
.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
context.writeDebug?.('scan', actionCommand);
|
||||
|
|
@ -113,7 +113,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.action(async (connectionId: string | undefined, options, command) => {
|
||||
if (!connectionId) {
|
||||
scan.outputHelp();
|
||||
context.io.stderr.write('klo dev scan requires <connectionId> or a subcommand\n');
|
||||
context.io.stderr.write('ktx dev scan requires <connectionId> or a subcommand\n');
|
||||
context.setExitCode(1);
|
||||
return;
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.argument('<runId>', 'Local scan run id')
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (runId: string, _options: unknown, command) => {
|
||||
await runScanArgs(context, {
|
||||
|
|
@ -152,7 +152,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.option('--json', 'Print the raw scan report JSON', false)
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (runId: string, options, command) => {
|
||||
await runScanArgs(context, {
|
||||
|
|
@ -189,7 +189,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.option('--json', 'Print relationship artifacts as JSON', false)
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (runId: string, options, command) => {
|
||||
const decision = relationshipDecisionArgs(options);
|
||||
|
|
@ -231,7 +231,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.option('--json', 'Print the apply result as JSON', false)
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (runId: string, options, command) => {
|
||||
const parentOptions = command.parent?.opts() as { dryRun?: boolean } | undefined;
|
||||
|
|
@ -249,7 +249,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
scan
|
||||
.command('relationship-feedback')
|
||||
.description('Export persisted relationship review decisions as calibration labels')
|
||||
.option('--connection <connectionId>', 'Only export labels for one KLO connection')
|
||||
.option('--connection <connectionId>', 'Only export labels for one KTX connection')
|
||||
.option(
|
||||
'--decision <decision>',
|
||||
'Relationship feedback decision: accepted, rejected, all',
|
||||
|
|
@ -260,7 +260,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.addOption(new Option('--jsonl', 'Print labels as newline-delimited JSON').default(false).conflicts('json'))
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (options, command) => {
|
||||
await runScanArgs(context, {
|
||||
|
|
@ -276,7 +276,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
scan
|
||||
.command('relationship-calibration')
|
||||
.description('Summarize relationship feedback labels against current score thresholds')
|
||||
.option('--connection <connectionId>', 'Only calibrate labels for one KLO connection')
|
||||
.option('--connection <connectionId>', 'Only calibrate labels for one KTX connection')
|
||||
.option(
|
||||
'--decision <decision>',
|
||||
'Relationship feedback decision: accepted, rejected, all',
|
||||
|
|
@ -298,7 +298,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.option('--json', 'Print the calibration report as JSON', false)
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (options, command) => {
|
||||
await runScanArgs(context, {
|
||||
|
|
@ -315,7 +315,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
scan
|
||||
.command('relationship-thresholds')
|
||||
.description('Evaluate relationship feedback labels for offline threshold advice')
|
||||
.option('--connection <connectionId>', 'Only evaluate labels for one KLO connection')
|
||||
.option('--connection <connectionId>', 'Only evaluate labels for one KTX connection')
|
||||
.option(
|
||||
'--min-total-labels <count>',
|
||||
'Minimum scored labels before advice can be ready',
|
||||
|
|
@ -337,7 +337,7 @@ export function registerScanCommands(program: Command, context: KloCliCommandCon
|
|||
.option('--json', 'Print the threshold advice report as JSON', false)
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\n--project-dir is inherited from `klo dev scan` (default: KLO_PROJECT_DIR or current working directory).\n',
|
||||
'\n--project-dir is inherited from `ktx dev scan` (default: KTX_PROJECT_DIR or current working directory).\n',
|
||||
)
|
||||
.action(async (options, command) => {
|
||||
await runScanArgs(context, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type Command, InvalidArgumentError } from '@commander-js/extra-typings';
|
||||
import { type KloCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloServeArgs } from '../serve.js';
|
||||
import { type KtxCliCommandContext, resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KtxServeArgs } from '../serve.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/serve-commands');
|
||||
|
|
@ -12,10 +12,10 @@ function parseMcp(value: string): 'stdio' {
|
|||
throw new InvalidArgumentError('Only stdio is supported in this phase');
|
||||
}
|
||||
|
||||
export function registerServeCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerServeCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
program
|
||||
.command('serve')
|
||||
.description('Run standalone KLO services such as MCP stdio')
|
||||
.description('Run standalone KTX services such as MCP stdio')
|
||||
.requiredOption('--mcp <mode>', 'MCP transport mode', parseMcp)
|
||||
.option('--user-id <id>', 'Local user id', 'local')
|
||||
.option('--semantic-compute', 'Enable semantic-layer compute', false)
|
||||
|
|
@ -30,7 +30,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
|
|||
if (options.executeQueries === true && !semanticCompute) {
|
||||
throw new Error('--execute-queries requires --semantic-compute');
|
||||
}
|
||||
const args: KloServeArgs = {
|
||||
const args: KtxServeArgs = {
|
||||
mcp: options.mcp,
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
userId: options.userId,
|
||||
|
|
@ -41,7 +41,7 @@ export function registerServeCommands(program: Command, context: KloCliCommandCo
|
|||
memoryCapture: options.memoryCapture === true,
|
||||
memoryModel: options.memoryModel,
|
||||
};
|
||||
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKloServeStdio;
|
||||
const runner = context.deps.serveStdio ?? (await import('../serve.js')).runKtxServeStdio;
|
||||
context.setExitCode(await runner(args));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
||||
import type { KloCliCommandContext } from '../cli-program.js';
|
||||
import type { KtxCliCommandContext } from '../cli-program.js';
|
||||
import { resolveCommandProjectDir } from '../cli-program.js';
|
||||
import type { KloSetupDatabaseDriver } from '../setup-databases.js';
|
||||
import type { KloSetupSourceType } from '../setup-sources.js';
|
||||
import type { KtxSetupDatabaseDriver } from '../setup-databases.js';
|
||||
import type { KtxSetupSourceType } from '../setup-sources.js';
|
||||
import { registerDemoCommands } from './demo-commands.js';
|
||||
|
||||
async function runSetupArgs(
|
||||
context: KloCliCommandContext,
|
||||
context: KtxCliCommandContext,
|
||||
args: Parameters<NonNullable<typeof context.deps.setup>>[0],
|
||||
) {
|
||||
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
|
||||
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ function embeddingBackend(value: string): 'openai' | 'sentence-transformers' {
|
|||
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
||||
}
|
||||
|
||||
function databaseDriver(value: string): KloSetupDatabaseDriver {
|
||||
function databaseDriver(value: string): KtxSetupDatabaseDriver {
|
||||
if (
|
||||
value === 'sqlite' ||
|
||||
value === 'postgres' ||
|
||||
|
|
@ -43,7 +43,7 @@ function databaseDriver(value: string): KloSetupDatabaseDriver {
|
|||
throw new InvalidArgumentError(`invalid choice '${value}'`);
|
||||
}
|
||||
|
||||
function sourceType(value: string): KloSetupSourceType {
|
||||
function sourceType(value: string): KtxSetupSourceType {
|
||||
if (
|
||||
value === 'dbt' ||
|
||||
value === 'metricflow' ||
|
||||
|
|
@ -109,7 +109,7 @@ function shouldShowSetupEntryMenu(
|
|||
embeddingApiKeyEnv?: string;
|
||||
embeddingApiKeyFile?: string;
|
||||
skipEmbeddings?: boolean;
|
||||
database?: KloSetupDatabaseDriver[];
|
||||
database?: KtxSetupDatabaseDriver[];
|
||||
databaseConnectionId?: string[];
|
||||
newDatabaseConnectionId?: string;
|
||||
databaseUrl?: string;
|
||||
|
|
@ -121,7 +121,7 @@ function shouldShowSetupEntryMenu(
|
|||
historicSqlServiceAccountPattern?: string[];
|
||||
historicSqlRedactionPattern?: string[];
|
||||
skipDatabases?: boolean;
|
||||
source?: KloSetupSourceType;
|
||||
source?: KtxSetupSourceType;
|
||||
sourceConnectionId?: string;
|
||||
sourcePath?: string;
|
||||
sourceGitUrl?: string;
|
||||
|
|
@ -210,13 +210,13 @@ function shouldShowSetupEntryMenu(
|
|||
].some((optionName) => optionWasSpecified(command, optionName));
|
||||
}
|
||||
|
||||
export function registerSetupCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerSetupCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const setup = program
|
||||
.command('setup')
|
||||
.description('Set up or resume a local KLO project')
|
||||
.option('--project-dir <path>', 'KLO project directory')
|
||||
.option('--new', 'Create a new KLO project before setup', false)
|
||||
.option('--existing', 'Use an existing KLO project', false)
|
||||
.description('Set up or resume a local KTX project')
|
||||
.option('--project-dir <path>', 'KTX project directory')
|
||||
.option('--new', 'Create a new KTX project before setup', false)
|
||||
.option('--existing', 'Use an existing KTX project', false)
|
||||
.option('--agents', 'Install agent integration only', false)
|
||||
.addOption(
|
||||
new Option('--target <target>', 'Agent target').choices([
|
||||
|
|
@ -247,10 +247,10 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
|
|||
.option(
|
||||
'--database <driver>',
|
||||
'Database driver to configure; repeatable',
|
||||
(value, previous: KloSetupDatabaseDriver[]) => {
|
||||
(value, previous: KtxSetupDatabaseDriver[]) => {
|
||||
return [...previous, databaseDriver(value)];
|
||||
},
|
||||
[] as KloSetupDatabaseDriver[],
|
||||
[] as KtxSetupDatabaseDriver[],
|
||||
)
|
||||
.option(
|
||||
'--database-connection-id <id>',
|
||||
|
|
@ -291,7 +291,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
|
|||
(value, previous: string[]) => [...previous, value],
|
||||
[],
|
||||
)
|
||||
.option('--skip-databases', 'Leave database setup incomplete; KLO cannot work until a primary source is added', false)
|
||||
.option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a primary source is added', false)
|
||||
.addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType))
|
||||
.option('--source-connection-id <id>', 'Connection id for source setup')
|
||||
.option('--source-path <path>', 'Local source path for dbt, MetricFlow, or LookML')
|
||||
|
|
@ -421,9 +421,9 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
|
|||
});
|
||||
});
|
||||
|
||||
registerDemoCommands(setup, context, { description: 'Run the packaged KLO demo from setup' });
|
||||
registerDemoCommands(setup, context, { description: 'Run the packaged KTX demo from setup' });
|
||||
|
||||
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KLO context');
|
||||
const setupContext = setup.command('context').description('Build, inspect, and recover setup-managed KTX context');
|
||||
|
||||
function setupContextInputMode(command: {
|
||||
optsWithGlobals?: () => unknown;
|
||||
|
|
@ -435,7 +435,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
|
|||
|
||||
setupContext
|
||||
.command('build')
|
||||
.description('Build agent-ready KLO context for setup')
|
||||
.description('Build agent-ready KTX context for setup')
|
||||
.option('--no-input', 'Disable interactive terminal input')
|
||||
.action(async (options: { input?: boolean }, command) => {
|
||||
await runSetupArgs(context, {
|
||||
|
|
@ -505,7 +505,7 @@ export function registerSetupCommands(program: Command, context: KloCliCommandCo
|
|||
|
||||
setup
|
||||
.command('status')
|
||||
.description('Show setup readiness for the resolved KLO project')
|
||||
.description('Show setup readiness for the resolved KTX project')
|
||||
.option('--json', 'Print JSON output', false)
|
||||
.action(async (options: { json?: boolean }, command) => {
|
||||
await runSetupArgs(context, {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings';
|
||||
import {
|
||||
collectOption,
|
||||
type KloCliCommandContext,
|
||||
type KtxCliCommandContext,
|
||||
parsePositiveIntegerOption,
|
||||
resolveCommandProjectDir,
|
||||
} from '../cli-program.js';
|
||||
import { slQueryCommandSchema } from '../command-schemas.js';
|
||||
import type { KloSlArgs } from '../sl.js';
|
||||
import type { KtxSlArgs } from '../sl.js';
|
||||
import { profileMark } from '../startup-profile.js';
|
||||
|
||||
profileMark('module:commands/sl-commands');
|
||||
|
|
@ -32,24 +32,24 @@ function collectOrderBy(
|
|||
return [...previous, parseOrderBy(value)];
|
||||
}
|
||||
|
||||
async function runSlArgs(context: KloCliCommandContext, args: KloSlArgs): Promise<void> {
|
||||
const runner = context.deps.sl ?? (await import('../sl.js')).runKloSl;
|
||||
async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise<void> {
|
||||
const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl;
|
||||
context.setExitCode(await runner(args, context.io));
|
||||
}
|
||||
|
||||
export function registerSlCommands(program: Command, context: KloCliCommandContext, commandName = 'sl'): void {
|
||||
export function registerSlCommands(program: Command, context: KtxCliCommandContext, commandName = 'sl'): void {
|
||||
const sl = program
|
||||
.command(commandName)
|
||||
.description('List, read, validate, query, or write local semantic-layer sources')
|
||||
.showHelpAfterError()
|
||||
.addHelpText(
|
||||
'after',
|
||||
'\nProject directory defaults to KLO_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
'\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n',
|
||||
);
|
||||
|
||||
sl.command('list')
|
||||
.description('List semantic-layer sources')
|
||||
.option('--connection-id <id>', 'KLO connection id')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.addOption(
|
||||
new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
|
||||
'pretty',
|
||||
|
|
@ -71,7 +71,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
|
|||
sl.command('read')
|
||||
.description('Read a semantic-layer source')
|
||||
.argument('<sourceName>', 'Semantic-layer source name')
|
||||
.requiredOption('--connection-id <id>', 'KLO connection id')
|
||||
.requiredOption('--connection-id <id>', 'KTX connection id')
|
||||
.action(async (sourceName: string, options: { connectionId: string }, command) => {
|
||||
await runSlArgs(context, {
|
||||
command: 'read',
|
||||
|
|
@ -84,7 +84,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
|
|||
sl.command('validate')
|
||||
.description('Validate a semantic-layer source')
|
||||
.argument('<sourceName>', 'Semantic-layer source name')
|
||||
.requiredOption('--connection-id <id>', 'KLO connection id')
|
||||
.requiredOption('--connection-id <id>', 'KTX connection id')
|
||||
.action(async (sourceName: string, options: { connectionId: string }, command) => {
|
||||
await runSlArgs(context, {
|
||||
command: 'validate',
|
||||
|
|
@ -97,7 +97,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
|
|||
sl.command('write')
|
||||
.description('Write a semantic-layer source')
|
||||
.argument('<sourceName>', 'Semantic-layer source name')
|
||||
.requiredOption('--connection-id <id>', 'KLO connection id')
|
||||
.requiredOption('--connection-id <id>', 'KTX connection id')
|
||||
.requiredOption('--yaml <yaml>', 'Semantic-layer source YAML')
|
||||
.action(async (sourceName: string, options: { connectionId: string; yaml: string }, command) => {
|
||||
await runSlArgs(context, {
|
||||
|
|
@ -111,7 +111,7 @@ export function registerSlCommands(program: Command, context: KloCliCommandConte
|
|||
|
||||
sl.command('query')
|
||||
.description('Compile or execute a semantic-layer query')
|
||||
.option('--connection-id <id>', 'KLO connection id')
|
||||
.option('--connection-id <id>', 'KTX connection id')
|
||||
.option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
|
||||
.option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
|
||||
.option('--filter <filter>', 'Filter expression; repeatable', collectOption, [])
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import type { Command } from '@commander-js/extra-typings';
|
||||
import type { KloCliCommandContext } from '../cli-program.js';
|
||||
import type { KtxCliCommandContext } from '../cli-program.js';
|
||||
import { resolveCommandProjectDir } from '../cli-program.js';
|
||||
|
||||
export function registerStatusCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerStatusCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
program
|
||||
.command('status')
|
||||
.description('Show current KLO project setup status')
|
||||
.description('Show current KTX project setup status')
|
||||
.option('--json', 'Print JSON output', false)
|
||||
.action(async (options: { json?: boolean }, command) => {
|
||||
const runner = context.deps.setup ?? (await import('../setup.js')).runKloSetup;
|
||||
const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
|
||||
context.setExitCode(
|
||||
await runner(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ export interface ZshCompletionInstallResult {
|
|||
zshrcPath: string;
|
||||
}
|
||||
|
||||
const KLO_COMPLETION_BLOCK_START = '# >>> klo completion >>>';
|
||||
const KLO_COMPLETION_BLOCK_END = '# <<< klo completion <<<';
|
||||
const KLO_COMPLETION_BLOCK_PATTERN = new RegExp(
|
||||
`\\n?${escapeRegExp(KLO_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KLO_COMPLETION_BLOCK_END)}\\n?`,
|
||||
const KTX_COMPLETION_BLOCK_START = '# >>> ktx completion >>>';
|
||||
const KTX_COMPLETION_BLOCK_END = '# <<< ktx completion <<<';
|
||||
const KTX_COMPLETION_BLOCK_PATTERN = new RegExp(
|
||||
`\\n?${escapeRegExp(KTX_COMPLETION_BLOCK_START)}[\\s\\S]*?${escapeRegExp(KTX_COMPLETION_BLOCK_END)}\\n?`,
|
||||
'g',
|
||||
);
|
||||
|
||||
|
|
@ -39,27 +39,27 @@ export function zshCompletionScript(): string {
|
|||
const zshWords = '$' + '{words[@]}';
|
||||
const zshCompletionCapture = [
|
||||
'$',
|
||||
`{(@f)$("${'$'}{klo_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
|
||||
`{(@f)$("${'$'}{ktx_completion_command[@]}" dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}" 2>/dev/null)}`,
|
||||
].join('');
|
||||
const zshCompletionsCount = '$' + '{#completions[@]}';
|
||||
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KLO_COMPLETION_COMMAND:-klo}")';
|
||||
const zshCompletionCommand = '$' + '(eval "print -r -- $' + '{KTX_COMPLETION_COMMAND:-ktx}")';
|
||||
|
||||
return [
|
||||
'#compdef klo',
|
||||
'#compdef ktx',
|
||||
'',
|
||||
'_klo() {',
|
||||
'_ktx() {',
|
||||
' local -a completions',
|
||||
' local -a klo_completion_command',
|
||||
` klo_completion_command=("\${(@z)${zshCompletionCommand}}")`,
|
||||
' local -a ktx_completion_command',
|
||||
` ktx_completion_command=("\${(@z)${zshCompletionCommand}}")`,
|
||||
` completions=("${zshCompletionCapture}")`,
|
||||
` if (( ${zshCompletionsCount} )); then`,
|
||||
" _describe 'klo completions' completions",
|
||||
" _describe 'ktx completions' completions",
|
||||
' else',
|
||||
' _files',
|
||||
' fi',
|
||||
'}',
|
||||
'',
|
||||
'compdef _klo klo',
|
||||
'compdef _ktx ktx',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ export async function installZshCompletion(): Promise<ZshCompletionInstallResult
|
|||
const homeDir = process.env.HOME || homedir();
|
||||
const zshConfigDir = process.env.ZDOTDIR || homeDir;
|
||||
const completionDir = join(homeDir, '.zfunc');
|
||||
const completionPath = join(completionDir, '_klo');
|
||||
const completionPath = join(completionDir, '_ktx');
|
||||
const zshrcPath = join(zshConfigDir, '.zshrc');
|
||||
|
||||
await mkdir(completionDir, { recursive: true });
|
||||
|
|
@ -290,7 +290,7 @@ async function readOptionalTextFile(path: string): Promise<string> {
|
|||
}
|
||||
|
||||
function updateZshrcCompletionBlock(contents: string): string {
|
||||
const withoutManagedBlock = contents.replace(KLO_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
|
||||
const withoutManagedBlock = contents.replace(KTX_COMPLETION_BLOCK_PATTERN, normalizeTrailingNewline);
|
||||
const hasCompinit = /^.*\bcompinit\b.*$/m.test(withoutManagedBlock);
|
||||
const block = zshrcCompletionBlock({ includeCompinit: !hasCompinit });
|
||||
|
||||
|
|
@ -313,23 +313,23 @@ function updateZshrcCompletionBlock(contents: string): string {
|
|||
|
||||
function zshrcCompletionBlock(options: { includeCompinit: boolean }): string {
|
||||
return [
|
||||
KLO_COMPLETION_BLOCK_START,
|
||||
'_klo_completion_command() {',
|
||||
KTX_COMPLETION_BLOCK_START,
|
||||
'_ktx_completion_command() {',
|
||||
' local dir="$PWD"',
|
||||
' while [[ "$dir" != "/" ]]; do',
|
||||
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "klo-workspace"' "$dir/package.json" 2>/dev/null; then`,
|
||||
' print -r -- "node $dir/scripts/run-klo.mjs --"',
|
||||
` if [[ -f "$dir/package.json" ]] && command grep -q '"name": "ktx-workspace"' "$dir/package.json" 2>/dev/null; then`,
|
||||
' print -r -- "node $dir/scripts/run-ktx.mjs --"',
|
||||
' return',
|
||||
' fi',
|
||||
' dir="' + '$' + '{dir:h}"',
|
||||
' done',
|
||||
' print -r -- "klo"',
|
||||
' print -r -- "ktx"',
|
||||
'}',
|
||||
"export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'",
|
||||
"export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'",
|
||||
'setopt complete_aliases',
|
||||
'fpath=("$HOME/.zfunc" $fpath)',
|
||||
...(options.includeCompinit ? ['autoload -Uz compinit', 'compinit'] : []),
|
||||
KLO_COMPLETION_BLOCK_END,
|
||||
KTX_COMPLETION_BLOCK_END,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKloProject, parseKloProjectConfig } from '@klo/context/project';
|
||||
import type { KloConnectionDriver, KloScanConnector, KloSchemaSnapshot } from '@klo/context/scan';
|
||||
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
|
||||
import type { KtxConnectionDriver, KtxScanConnector, KtxSchemaSnapshot } from '@ktx/context/scan';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runKloConnection } from './connection.js';
|
||||
import { runKloCli, type KloCliIo } from './index.js';
|
||||
import { runKtxConnection } from './connection.js';
|
||||
import { runKtxCli, type KtxCliIo } from './index.js';
|
||||
|
||||
function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
|
||||
let stdout = '';
|
||||
|
|
@ -32,7 +32,7 @@ function makeIo(options: { stdoutIsTty?: boolean; stdinIsTty?: boolean } = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSchemaSnapshot {
|
||||
function snapshotFor(driver: KtxConnectionDriver, tableNames: string[]): KtxSchemaSnapshot {
|
||||
return {
|
||||
connectionId: 'warehouse',
|
||||
driver,
|
||||
|
|
@ -52,10 +52,10 @@ function snapshotFor(driver: KloConnectionDriver, tableNames: string[]): KloSche
|
|||
};
|
||||
}
|
||||
|
||||
function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
|
||||
function nativeConnector(driver: KtxConnectionDriver, tableNames: string[]) {
|
||||
const introspect = vi.fn(async () => snapshotFor(driver, tableNames));
|
||||
const cleanup = vi.fn(async () => undefined);
|
||||
const connector: KloScanConnector = {
|
||||
const connector: KtxScanConnector = {
|
||||
id: `${driver}:warehouse`,
|
||||
driver,
|
||||
capabilities: {
|
||||
|
|
@ -75,11 +75,11 @@ function nativeConnector(driver: KloConnectionDriver, tableNames: string[]) {
|
|||
return { connector, introspect, cleanup };
|
||||
}
|
||||
|
||||
describe('runKloConnection', () => {
|
||||
describe('runKtxConnection', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-connection-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-connection-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -88,11 +88,11 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('adds and lists env-referenced connections without resolving secrets', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -109,18 +109,18 @@ describe('runKloConnection', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Connection: warehouse');
|
||||
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('url: env:DATABASE_URL');
|
||||
|
||||
const listIo = makeIo();
|
||||
await expect(runKloConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxConnection({ command: 'list', projectDir }, listIo.io)).resolves.toBe(0);
|
||||
expect(listIo.stdout()).toContain('warehouse');
|
||||
expect(listIo.stdout()).toContain('postgres');
|
||||
});
|
||||
|
||||
it('removes a configured connection from klo.yaml without deleting local artifacts when forced', async () => {
|
||||
it('removes a configured connection from ktx.yaml without deleting local artifacts when forced', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -134,14 +134,14 @@ describe('runKloConnection', () => {
|
|||
},
|
||||
makeIo().io,
|
||||
);
|
||||
const artifactPath = join(projectDir, '.klo', 'artifacts', 'warehouse.txt');
|
||||
await mkdir(join(projectDir, '.klo', 'artifacts'), { recursive: true });
|
||||
const artifactPath = join(projectDir, '.ktx', 'artifacts', 'warehouse.txt');
|
||||
await mkdir(join(projectDir, '.ktx', 'artifacts'), { recursive: true });
|
||||
await writeFile(artifactPath, 'keep me', 'utf-8');
|
||||
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'remove',
|
||||
projectDir,
|
||||
|
|
@ -153,20 +153,20 @@ describe('runKloConnection', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const parsed = parseKloProjectConfig(await readFile(join(projectDir, 'klo.yaml'), 'utf-8'));
|
||||
const parsed = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(parsed.connections.warehouse).toBeUndefined();
|
||||
await expect(readFile(artifactPath, 'utf-8')).resolves.toBe('keep me');
|
||||
expect(io.stdout()).toContain('Connection removed from klo.yaml.');
|
||||
expect(io.stdout()).toContain('Connection removed from ktx.yaml.');
|
||||
expect(io.stdout()).toContain(
|
||||
'Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.',
|
||||
'Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.',
|
||||
);
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('requires --force when removing in non-interactive mode', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -183,7 +183,7 @@ describe('runKloConnection', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'remove',
|
||||
projectDir,
|
||||
|
|
@ -200,11 +200,11 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('returns a clear error when removing an unknown connection', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'remove',
|
||||
projectDir,
|
||||
|
|
@ -216,13 +216,13 @@ describe('runKloConnection', () => {
|
|||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain('Connection "missing" is not configured in klo.yaml');
|
||||
expect(io.stderr()).toContain('Connection "missing" is not configured in ktx.yaml');
|
||||
});
|
||||
|
||||
it('asks for confirmation before removing in an interactive terminal', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -243,7 +243,7 @@ describe('runKloConnection', () => {
|
|||
};
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'remove',
|
||||
projectDir,
|
||||
|
|
@ -256,14 +256,14 @@ describe('runKloConnection', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(prompts.confirm).toHaveBeenCalledWith({
|
||||
message: 'Remove connection "warehouse" from klo.yaml? Ingested artifacts will remain in .klo/.',
|
||||
message: 'Remove connection "warehouse" from ktx.yaml? Ingested artifacts will remain in .ktx/.',
|
||||
initialValue: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('runs public connect map as refresh, validate, and list over the low-level mapping runner', async () => {
|
||||
const io = makeIo();
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
|
||||
if (argv[0] === 'refresh') {
|
||||
mappingIo.stdout.write('Discovery: 1 database\n');
|
||||
mappingIo.stdout.write('Unmapped discovered: 1\n');
|
||||
|
|
@ -282,7 +282,7 @@ describe('runKloConnection', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
|
||||
io.io,
|
||||
{ runMapping },
|
||||
|
|
@ -309,14 +309,14 @@ describe('runKloConnection', () => {
|
|||
expect(io.stdout()).toContain('Mappings:');
|
||||
expect(io.stdout()).toContain('1 -> [unmapped]');
|
||||
expect(io.stdout()).toContain('Next:');
|
||||
expect(io.stdout()).toContain('klo ingest prod-metabase');
|
||||
expect(io.stdout()).toContain('klo dev mapping');
|
||||
expect(io.stdout()).toContain('ktx ingest prod-metabase');
|
||||
expect(io.stdout()).toContain('ktx dev mapping');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('prints stable JSON for public connect map without leaking low-level stdout', async () => {
|
||||
const io = makeIo();
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
|
||||
if (argv[0] === 'refresh') {
|
||||
mappingIo.stdout.write('Discovery: 1 connection\nUnmapped discovered: 0\nStale mappings: 0\n');
|
||||
return 0;
|
||||
|
|
@ -332,8 +332,8 @@ describe('runKloConnection', () => {
|
|||
[
|
||||
{
|
||||
lookerConnectionName: 'analytics',
|
||||
kloConnectionId: 'prod-warehouse',
|
||||
source: 'klo.yaml',
|
||||
ktxConnectionId: 'prod-warehouse',
|
||||
source: 'ktx.yaml',
|
||||
},
|
||||
],
|
||||
null,
|
||||
|
|
@ -346,7 +346,7 @@ describe('runKloConnection', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-looker', json: true },
|
||||
io.io,
|
||||
{ runMapping },
|
||||
|
|
@ -357,7 +357,7 @@ describe('runKloConnection', () => {
|
|||
connectionId: string;
|
||||
refresh: { ok: boolean; output: string[] };
|
||||
validation: { ok: boolean; output: string[] };
|
||||
mappings: Array<{ lookerConnectionName: string; kloConnectionId: string; source: string }>;
|
||||
mappings: Array<{ lookerConnectionName: string; ktxConnectionId: string; source: string }>;
|
||||
};
|
||||
expect(parsed).toEqual({
|
||||
connectionId: 'prod-looker',
|
||||
|
|
@ -372,8 +372,8 @@ describe('runKloConnection', () => {
|
|||
mappings: [
|
||||
{
|
||||
lookerConnectionName: 'analytics',
|
||||
kloConnectionId: 'prod-warehouse',
|
||||
source: 'klo.yaml',
|
||||
ktxConnectionId: 'prod-warehouse',
|
||||
source: 'ktx.yaml',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -382,7 +382,7 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('returns the refresh failure when public connect map cannot discover source metadata', async () => {
|
||||
const io = makeIo();
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KloCliIo) => {
|
||||
const runMapping = vi.fn(async (argv: string[], mappingIo: KtxCliIo) => {
|
||||
if (argv[0] === 'refresh') {
|
||||
mappingIo.stderr.write('Metabase API key is not configured\n');
|
||||
return 1;
|
||||
|
|
@ -391,7 +391,7 @@ describe('runKloConnection', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{ command: 'map', projectDir: '/tmp/project', sourceConnectionId: 'prod-metabase', json: false },
|
||||
io.io,
|
||||
{ runMapping },
|
||||
|
|
@ -405,11 +405,11 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('rejects literal credential URLs unless explicitly allowed', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -430,12 +430,12 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('warns before writing explicitly allowed literal credential URLs without echoing the URL', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
const literalUrl = 'postgres://localhost:5432/warehouse';
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -452,19 +452,19 @@ describe('runKloConnection', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stderr()).toContain(
|
||||
'Warning: writing a literal credential URL to klo.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
|
||||
'Warning: writing a literal credential URL to ktx.yaml for connection "warehouse". Prefer env:NAME or file:/path references.',
|
||||
);
|
||||
expect(io.stderr()).not.toContain(literalUrl);
|
||||
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain(literalUrl);
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain(literalUrl);
|
||||
});
|
||||
|
||||
it('adds a Notion connection without writing token values', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection(
|
||||
runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -490,7 +490,7 @@ describe('runKloConnection', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(yaml).toContain('driver: notion');
|
||||
expect(yaml).toContain('auth_token_ref: env:NOTION_AUTH_TOKEN');
|
||||
expect(yaml).toContain('crawl_mode: all_accessible');
|
||||
|
|
@ -502,8 +502,8 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('runs connection notion pick --no-input through the public connection entrypoint', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -530,7 +530,7 @@ describe('runKloConnection', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'connection',
|
||||
'notion',
|
||||
|
|
@ -546,7 +546,7 @@ describe('runKloConnection', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const yaml = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const yaml = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(yaml).toContain('crawl_mode: selected_roots');
|
||||
expect(yaml).toContain('11111111-2222-3333-4444-555555555555');
|
||||
expect(yaml).toContain('database-1');
|
||||
|
|
@ -556,8 +556,8 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('tests a configured connection through the native scan connector', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -576,7 +576,7 @@ describe('runKloConnection', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
|
||||
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
|
||||
createScanConnector,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -600,8 +600,8 @@ describe('runKloConnection', () => {
|
|||
|
||||
it('cleans up the native scan connector when connection testing fails', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKloConnection(
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await runKtxConnection(
|
||||
{
|
||||
command: 'add',
|
||||
projectDir,
|
||||
|
|
@ -616,7 +616,7 @@ describe('runKloConnection', () => {
|
|||
makeIo().io,
|
||||
);
|
||||
const cleanup = vi.fn(async () => undefined);
|
||||
const connector: KloScanConnector = {
|
||||
const connector: KtxScanConnector = {
|
||||
id: 'sqlite:warehouse',
|
||||
driver: 'sqlite',
|
||||
capabilities: {
|
||||
|
|
@ -638,7 +638,7 @@ describe('runKloConnection', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
|
||||
runKtxConnection({ command: 'test', projectDir, connectionId: 'warehouse' }, io.io, {
|
||||
createScanConnector: vi.fn(async () => connector),
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { cancel, confirm, isCancel } from '@clack/prompts';
|
||||
import { type KloLocalProject, loadKloProject, serializeKloProjectConfig } from '@klo/context/project';
|
||||
import type { KloScanConnector } from '@klo/context/scan';
|
||||
import type { KloConnectionMappingArgs } from './commands/connection-mapping.js';
|
||||
import type { KloCliIo } from './index.js';
|
||||
import { createKloCliScanConnector } from './local-scan-connectors.js';
|
||||
import { type KtxLocalProject, loadKtxProject, serializeKtxProjectConfig } from '@ktx/context/project';
|
||||
import type { KtxScanConnector } from '@ktx/context/scan';
|
||||
import type { KtxConnectionMappingArgs } from './commands/connection-mapping.js';
|
||||
import type { KtxCliIo } from './index.js';
|
||||
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
||||
import { profileMark } from './startup-profile.js';
|
||||
|
||||
profileMark('module:connection');
|
||||
|
||||
interface KloNotionConnectionCliConfig {
|
||||
interface KtxNotionConnectionCliConfig {
|
||||
authTokenRef: string;
|
||||
crawlMode: 'all_accessible' | 'selected_roots';
|
||||
rootPageIds: string[];
|
||||
|
|
@ -19,9 +19,9 @@ interface KloNotionConnectionCliConfig {
|
|||
maxKnowledgeUpdatesPerRun?: number;
|
||||
}
|
||||
|
||||
type KloConnectionInputMode = 'disabled';
|
||||
type KtxConnectionInputMode = 'disabled';
|
||||
|
||||
export type KloConnectionArgs =
|
||||
export type KtxConnectionArgs =
|
||||
| { command: 'list'; projectDir: string }
|
||||
| {
|
||||
command: 'add';
|
||||
|
|
@ -33,7 +33,7 @@ export type KloConnectionArgs =
|
|||
readonly: boolean;
|
||||
force: boolean;
|
||||
allowLiteralCredentials: boolean;
|
||||
notion?: KloNotionConnectionCliConfig;
|
||||
notion?: KtxNotionConnectionCliConfig;
|
||||
}
|
||||
| { command: 'test'; projectDir: string; connectionId: string }
|
||||
| {
|
||||
|
|
@ -41,7 +41,7 @@ export type KloConnectionArgs =
|
|||
projectDir: string;
|
||||
connectionId: string;
|
||||
force: boolean;
|
||||
inputMode?: KloConnectionInputMode;
|
||||
inputMode?: KtxConnectionInputMode;
|
||||
}
|
||||
| {
|
||||
command: 'map';
|
||||
|
|
@ -50,19 +50,19 @@ export type KloConnectionArgs =
|
|||
json: boolean;
|
||||
};
|
||||
|
||||
interface KloConnectionPromptAdapter {
|
||||
interface KtxConnectionPromptAdapter {
|
||||
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
|
||||
cancel(message: string): void;
|
||||
}
|
||||
|
||||
interface KloConnectionIo extends KloCliIo {
|
||||
interface KtxConnectionIo extends KtxCliIo {
|
||||
stdin?: { isTTY?: boolean };
|
||||
}
|
||||
|
||||
interface KloConnectionDeps {
|
||||
createScanConnector?: typeof createKloCliScanConnector;
|
||||
runMapping?: (argv: string[], io: KloCliIo) => Promise<number>;
|
||||
prompts?: KloConnectionPromptAdapter;
|
||||
interface KtxConnectionDeps {
|
||||
createScanConnector?: typeof createKtxCliScanConnector;
|
||||
runMapping?: (argv: string[], io: KtxCliIo) => Promise<number>;
|
||||
prompts?: KtxConnectionPromptAdapter;
|
||||
}
|
||||
|
||||
function assertSafeConnectionId(connectionId: string): void {
|
||||
|
|
@ -76,10 +76,10 @@ function isCredentialReference(value: string): boolean {
|
|||
}
|
||||
|
||||
function literalCredentialWarning(connectionId: string): string {
|
||||
return `Warning: writing a literal credential URL to klo.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
|
||||
return `Warning: writing a literal credential URL to ktx.yaml for connection "${connectionId}". Prefer env:NAME or file:/path references.`;
|
||||
}
|
||||
|
||||
function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
|
||||
function createClackConnectionPromptAdapter(): KtxConnectionPromptAdapter {
|
||||
return {
|
||||
async confirm(options: { message: string; initialValue?: boolean }): Promise<boolean> {
|
||||
const value = await confirm(options);
|
||||
|
|
@ -92,24 +92,24 @@ function createClackConnectionPromptAdapter(): KloConnectionPromptAdapter {
|
|||
}
|
||||
|
||||
function isInteractiveConnectionIo(
|
||||
args: Extract<KloConnectionArgs, { command: 'remove' }>,
|
||||
io: KloConnectionIo,
|
||||
args: Extract<KtxConnectionArgs, { command: 'remove' }>,
|
||||
io: KtxConnectionIo,
|
||||
): boolean {
|
||||
return args.inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
|
||||
}
|
||||
|
||||
async function cleanupConnector(connector: KloScanConnector | null): Promise<void> {
|
||||
async function cleanupConnector(connector: KtxScanConnector | null): Promise<void> {
|
||||
if (connector?.cleanup) {
|
||||
await connector.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function testNativeConnection(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
createScanConnector: typeof createKloCliScanConnector,
|
||||
createScanConnector: typeof createKtxCliScanConnector,
|
||||
): Promise<{ driver: string; tableCount: number }> {
|
||||
let connector: KloScanConnector | null = null;
|
||||
let connector: KtxScanConnector | null = null;
|
||||
try {
|
||||
connector = await createScanConnector(project, connectionId);
|
||||
const snapshot = await connector.introspect(
|
||||
|
|
@ -131,7 +131,7 @@ async function testNativeConnection(
|
|||
}
|
||||
}
|
||||
|
||||
interface BufferedIo extends KloCliIo {
|
||||
interface BufferedIo extends KtxCliIo {
|
||||
stdoutText(): string;
|
||||
stderrText(): string;
|
||||
}
|
||||
|
|
@ -167,17 +167,17 @@ function splitOutputLines(output: string): string[] {
|
|||
}
|
||||
|
||||
async function runLowLevelMapping(
|
||||
args: KloConnectionMappingArgs,
|
||||
args: KtxConnectionMappingArgs,
|
||||
argv: string[],
|
||||
io: KloCliIo,
|
||||
deps: KloConnectionDeps,
|
||||
io: KtxCliIo,
|
||||
deps: KtxConnectionDeps,
|
||||
): Promise<number> {
|
||||
if (deps.runMapping) {
|
||||
return await deps.runMapping(argv, io);
|
||||
}
|
||||
|
||||
const { runKloConnectionMapping } = await import('./commands/connection-mapping.js');
|
||||
return await runKloConnectionMapping(args, io);
|
||||
const { runKtxConnectionMapping } = await import('./commands/connection-mapping.js');
|
||||
return await runKtxConnectionMapping(args, io);
|
||||
}
|
||||
|
||||
function parseMappingListJson(output: string): unknown[] {
|
||||
|
|
@ -190,12 +190,12 @@ function parseMappingListJson(output: string): unknown[] {
|
|||
}
|
||||
|
||||
async function runPublicConnectionMap(
|
||||
args: Extract<KloConnectionArgs, { command: 'map' }>,
|
||||
io: KloCliIo,
|
||||
deps: KloConnectionDeps,
|
||||
args: Extract<KtxConnectionArgs, { command: 'map' }>,
|
||||
io: KtxCliIo,
|
||||
deps: KtxConnectionDeps,
|
||||
): Promise<number> {
|
||||
const refreshIo = createBufferedIo();
|
||||
const refreshArgs: KloConnectionMappingArgs = {
|
||||
const refreshArgs: KtxConnectionMappingArgs = {
|
||||
command: 'refresh',
|
||||
projectDir: args.projectDir,
|
||||
connectionId: args.sourceConnectionId,
|
||||
|
|
@ -217,7 +217,7 @@ async function runPublicConnectionMap(
|
|||
}
|
||||
|
||||
const validationIo = createBufferedIo();
|
||||
const validationArgs: KloConnectionMappingArgs = {
|
||||
const validationArgs: KtxConnectionMappingArgs = {
|
||||
command: 'validate',
|
||||
projectDir: args.projectDir,
|
||||
connectionId: args.sourceConnectionId,
|
||||
|
|
@ -237,7 +237,7 @@ async function runPublicConnectionMap(
|
|||
|
||||
const listIo = createBufferedIo();
|
||||
const listArgv = ['list', args.sourceConnectionId, '--project-dir', args.projectDir];
|
||||
const listArgs: KloConnectionMappingArgs = {
|
||||
const listArgs: KtxConnectionMappingArgs = {
|
||||
command: 'list',
|
||||
projectDir: args.projectDir,
|
||||
connectionId: args.sourceConnectionId,
|
||||
|
|
@ -271,26 +271,26 @@ async function runPublicConnectionMap(
|
|||
io.stdout.write('\nMappings:\n');
|
||||
io.stdout.write(listIo.stdoutText().trim() ? listIo.stdoutText() : 'No mappings found.\n');
|
||||
io.stdout.write('\nNext:\n');
|
||||
io.stdout.write(` klo ingest ${args.sourceConnectionId}\n`);
|
||||
io.stdout.write(` klo dev mapping list ${args.sourceConnectionId}\n`);
|
||||
io.stdout.write(` ktx ingest ${args.sourceConnectionId}\n`);
|
||||
io.stdout.write(` ktx dev mapping list ${args.sourceConnectionId}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runKloConnection(
|
||||
args: KloConnectionArgs,
|
||||
io: KloConnectionIo = process,
|
||||
deps: KloConnectionDeps = {},
|
||||
export async function runKtxConnection(
|
||||
args: KtxConnectionArgs,
|
||||
io: KtxConnectionIo = process,
|
||||
deps: KtxConnectionDeps = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (args.command === 'map') {
|
||||
return await runPublicConnectionMap(args, io, deps);
|
||||
}
|
||||
|
||||
const project = await loadKloProject({ projectDir: args.projectDir });
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
if (args.command === 'list') {
|
||||
const entries = Object.entries(project.config.connections).sort(([a], [b]) => a.localeCompare(b));
|
||||
if (entries.length === 0) {
|
||||
io.stdout.write('No connections configured. Run `klo connection add <id> --driver <driver>` to add one.\n');
|
||||
io.stdout.write('No connections configured. Run `ktx connection add <id> --driver <driver>` to add one.\n');
|
||||
return 0;
|
||||
}
|
||||
const idWidth = Math.max('ID'.length, ...entries.map(([id]) => id.length));
|
||||
|
|
@ -348,11 +348,11 @@ export async function runKloConnection(
|
|||
},
|
||||
};
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig(nextConfig),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
`Update KLO connection: ${args.connectionId}`,
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig(nextConfig),
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
`Update KTX connection: ${args.connectionId}`,
|
||||
);
|
||||
io.stdout.write(`Connection: ${args.connectionId}\n`);
|
||||
io.stdout.write(`Driver: ${args.driver}\n`);
|
||||
|
|
@ -361,7 +361,7 @@ export async function runKloConnection(
|
|||
|
||||
if (args.command === 'remove') {
|
||||
if (!project.config.connections[args.connectionId]) {
|
||||
throw new Error(`Connection "${args.connectionId}" is not configured in klo.yaml`);
|
||||
throw new Error(`Connection "${args.connectionId}" is not configured in ktx.yaml`);
|
||||
}
|
||||
|
||||
if (!args.force) {
|
||||
|
|
@ -373,7 +373,7 @@ export async function runKloConnection(
|
|||
|
||||
const prompts = deps.prompts ?? createClackConnectionPromptAdapter();
|
||||
const confirmed = await prompts.confirm({
|
||||
message: `Remove connection "${args.connectionId}" from klo.yaml? Ingested artifacts will remain in .klo/.`,
|
||||
message: `Remove connection "${args.connectionId}" from ktx.yaml? Ingested artifacts will remain in .ktx/.`,
|
||||
initialValue: false,
|
||||
});
|
||||
if (!confirmed) {
|
||||
|
|
@ -388,21 +388,21 @@ export async function runKloConnection(
|
|||
connections,
|
||||
};
|
||||
await project.fileStore.writeFile(
|
||||
'klo.yaml',
|
||||
serializeKloProjectConfig(nextConfig),
|
||||
'klo',
|
||||
'klo@example.com',
|
||||
`Remove KLO connection: ${args.connectionId}`,
|
||||
'ktx.yaml',
|
||||
serializeKtxProjectConfig(nextConfig),
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
`Remove KTX connection: ${args.connectionId}`,
|
||||
);
|
||||
io.stdout.write('Connection removed from klo.yaml.\n');
|
||||
io.stdout.write('Ingested artifacts from this connection remain in .klo/. Run klo dev artifacts to inspect.\n');
|
||||
io.stdout.write('Connection removed from ktx.yaml.\n');
|
||||
io.stdout.write('Ingested artifacts from this connection remain in .ktx/. Run ktx dev artifacts to inspect.\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const result = await testNativeConnection(
|
||||
project,
|
||||
args.connectionId,
|
||||
deps.createScanConnector ?? createKloCliScanConnector,
|
||||
deps.createScanConnector ?? createKtxCliScanConnector,
|
||||
);
|
||||
io.stdout.write(`Connection test passed: ${args.connectionId}\n`);
|
||||
io.stdout.write(`Driver: ${result.driver}\n`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { buildDefaultKloProjectConfig, type KloProjectConfig } from '@klo/context/project';
|
||||
import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/context/project';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { KloPublicIngestProject, KloPublicIngestTargetResult } from './public-ingest.js';
|
||||
import type { KtxPublicIngestProject, KtxPublicIngestTargetResult } from './public-ingest.js';
|
||||
import {
|
||||
extractProgressMessage,
|
||||
initViewState,
|
||||
|
|
@ -32,17 +32,17 @@ function makeIo(options: { isTTY?: boolean } = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function projectWithConnections(connections: KloProjectConfig['connections']): KloPublicIngestProject {
|
||||
function projectWithConnections(connections: KtxProjectConfig['connections']): KtxPublicIngestProject {
|
||||
return {
|
||||
projectDir: '/tmp/project',
|
||||
config: {
|
||||
...buildDefaultKloProjectConfig('warehouse'),
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
connections,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
|
||||
function successResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
|
||||
return {
|
||||
connectionId,
|
||||
driver,
|
||||
|
|
@ -55,7 +55,7 @@ function successResult(connectionId: string, driver: string, operation: 'scan' |
|
|||
};
|
||||
}
|
||||
|
||||
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KloPublicIngestTargetResult {
|
||||
function failedResult(connectionId: string, driver: string, operation: 'scan' | 'source-ingest'): KtxPublicIngestTargetResult {
|
||||
return {
|
||||
connectionId,
|
||||
driver,
|
||||
|
|
@ -78,7 +78,7 @@ describe('extractProgressMessage', () => {
|
|||
});
|
||||
|
||||
it('returns null for non-progress output', () => {
|
||||
expect(extractProgressMessage('KLO scan completed\n')).toBeNull();
|
||||
expect(extractProgressMessage('KTX scan completed\n')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ describe('renderContextBuildView', () => {
|
|||
]);
|
||||
|
||||
const output = renderContextBuildView(state, { styled: false });
|
||||
expect(output).toContain('Building KLO context');
|
||||
expect(output).toContain('Building KTX context');
|
||||
expect(output).toContain('Primary sources:');
|
||||
expect(output).toContain('warehouse');
|
||||
expect(output).toContain('queued');
|
||||
|
|
@ -237,7 +237,7 @@ describe('runContextBuild', () => {
|
|||
);
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Building KLO context');
|
||||
expect(output).toContain('Building KTX context');
|
||||
expect(output).toContain('Primary sources:');
|
||||
expect(output).toContain('warehouse');
|
||||
expect(output).toContain('Context sources:');
|
||||
|
|
@ -297,7 +297,7 @@ describe('runContextBuild', () => {
|
|||
|
||||
expect(mockExit).toHaveBeenCalledWith(0);
|
||||
expect(io.stdout()).toContain('Context build continuing in the background.');
|
||||
expect(io.stdout()).toContain('Resume: klo setup --project-dir /tmp/project');
|
||||
expect(io.stdout()).toContain('Resume: ktx setup --project-dir /tmp/project');
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, openSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import type { KloCliIo } from './index.js';
|
||||
import type { KtxCliIo } from './index.js';
|
||||
import type {
|
||||
KloPublicIngestArgs,
|
||||
KloPublicIngestPlanTarget,
|
||||
KloPublicIngestProject,
|
||||
KloPublicIngestTargetResult,
|
||||
KtxPublicIngestArgs,
|
||||
KtxPublicIngestPlanTarget,
|
||||
KtxPublicIngestProject,
|
||||
KtxPublicIngestTargetResult,
|
||||
} from './public-ingest.js';
|
||||
import { buildPublicIngestPlan, executePublicIngestTarget } from './public-ingest.js';
|
||||
import { formatDuration } from './demo-metrics.js';
|
||||
|
|
@ -18,7 +18,7 @@ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
|
|||
const ESC = String.fromCharCode(0x1b);
|
||||
|
||||
export interface ContextBuildTargetState {
|
||||
target: KloPublicIngestPlanTarget;
|
||||
target: KtxPublicIngestPlanTarget;
|
||||
status: 'queued' | 'running' | 'done' | 'failed';
|
||||
detailLine: string | null;
|
||||
summaryText: string | null;
|
||||
|
|
@ -131,7 +131,7 @@ function renderTargetGroup(
|
|||
}
|
||||
|
||||
function resumeCommand(projectDir?: string): string {
|
||||
return projectDir ? `klo setup --project-dir ${projectDir}` : 'klo setup';
|
||||
return projectDir ? `ktx setup --project-dir ${projectDir}` : 'ktx setup';
|
||||
}
|
||||
|
||||
export function renderContextBuildView(
|
||||
|
|
@ -142,7 +142,7 @@ export function renderContextBuildView(
|
|||
const width = columnWidth(state);
|
||||
const lines: string[] = [
|
||||
'',
|
||||
'Building KLO context',
|
||||
'Building KTX context',
|
||||
'─────────────────────',
|
||||
...renderTargetGroup('Primary sources', state.primarySources, state.frame, styled, width),
|
||||
...renderTargetGroup('Context sources', state.contextSources, state.frame, styled, width),
|
||||
|
|
@ -184,7 +184,7 @@ export function parseIngestSummary(output: string): string | null {
|
|||
}
|
||||
|
||||
interface CapturedIo {
|
||||
io: KloCliIo;
|
||||
io: KtxCliIo;
|
||||
captured(): string;
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ function createCaptureIo(onProgress: (message: string) => void, isTTY: boolean):
|
|||
|
||||
// --- Repaint ---
|
||||
|
||||
function createRepainter(io: KloCliIo) {
|
||||
function createRepainter(io: KtxCliIo) {
|
||||
let lastLineCount = 0;
|
||||
|
||||
return {
|
||||
|
|
@ -229,7 +229,7 @@ function createRepainter(io: KloCliIo) {
|
|||
|
||||
// --- Background build ---
|
||||
|
||||
function resolveKloEntryScript(): string | null {
|
||||
function resolveKtxEntryScript(): string | null {
|
||||
const argv1 = process.argv[1];
|
||||
if (argv1 && (argv1.endsWith('.js') || argv1.endsWith('.ts') || argv1.endsWith('.mjs'))) {
|
||||
return argv1;
|
||||
|
|
@ -238,11 +238,11 @@ function resolveKloEntryScript(): string | null {
|
|||
}
|
||||
|
||||
function spawnBackgroundBuild(projectDir: string): { logPath: string } | null {
|
||||
const entryScript = resolveKloEntryScript();
|
||||
const entryScript = resolveKtxEntryScript();
|
||||
if (!entryScript) return null;
|
||||
|
||||
const resolvedDir = resolve(projectDir);
|
||||
const logDir = join(resolvedDir, '.klo', 'setup');
|
||||
const logDir = join(resolvedDir, '.ktx', 'setup');
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
const logPath = join(logDir, 'context-build.log');
|
||||
const logFd = openSync(logPath, 'w');
|
||||
|
|
@ -280,11 +280,11 @@ function defaultSetupKeystroke(onDetach: () => void, onCtrlC: () => void): (() =
|
|||
|
||||
// --- Orchestration ---
|
||||
|
||||
function makeTargetState(target: KloPublicIngestPlanTarget): ContextBuildTargetState {
|
||||
function makeTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState {
|
||||
return { target, status: 'queued', detailLine: null, summaryText: null, startedAt: null, elapsedMs: 0 };
|
||||
}
|
||||
|
||||
export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuildViewState {
|
||||
export function initViewState(targets: KtxPublicIngestPlanTarget[]): ContextBuildViewState {
|
||||
return {
|
||||
primarySources: targets.filter((t) => t.operation === 'scan').map(makeTargetState),
|
||||
contextSources: targets.filter((t) => t.operation === 'source-ingest').map(makeTargetState),
|
||||
|
|
@ -293,9 +293,9 @@ export function initViewState(targets: KloPublicIngestPlanTarget[]): ContextBuil
|
|||
}
|
||||
|
||||
export async function runContextBuild(
|
||||
project: KloPublicIngestProject,
|
||||
project: KtxPublicIngestProject,
|
||||
args: ContextBuildArgs,
|
||||
io: KloCliIo,
|
||||
io: KtxCliIo,
|
||||
deps: ContextBuildDeps = {},
|
||||
): Promise<ContextBuildResult> {
|
||||
const plan = buildPublicIngestPlan(project, { projectDir: args.projectDir, all: true });
|
||||
|
|
@ -339,7 +339,7 @@ export async function runContextBuild(
|
|||
const bg = spawnBackgroundBuild(args.projectDir);
|
||||
io.stdout.write('\n\nContext build continuing in the background.\n');
|
||||
if (bg) io.stdout.write(`Log: ${bg.logPath}\n`);
|
||||
io.stdout.write(`Status: klo setup context status --project-dir ${resolve(args.projectDir)}\n`);
|
||||
io.stdout.write(`Status: ktx setup context status --project-dir ${resolve(args.projectDir)}\n`);
|
||||
io.stdout.write(`Resume: ${resumeCommand(args.projectDir)}\n`);
|
||||
process.exit(0);
|
||||
},
|
||||
|
|
@ -351,7 +351,7 @@ export async function runContextBuild(
|
|||
},
|
||||
);
|
||||
}
|
||||
const runArgs: Extract<KloPublicIngestArgs, { command: 'run' }> = {
|
||||
const runArgs: Extract<KtxPublicIngestArgs, { command: 'run' }> = {
|
||||
command: 'run',
|
||||
projectDir: args.projectDir,
|
||||
all: true,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ async function readPackagedJson<T>(relativePath: string): Promise<T> {
|
|||
}
|
||||
|
||||
describe('demo assets', () => {
|
||||
const projectDir = join(tmpdir(), `klo-demo-assets-${process.pid}`);
|
||||
const projectDir = join(tmpdir(), `ktx-demo-assets-${process.pid}`);
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
|
|
@ -36,8 +36,8 @@ describe('demo assets', () => {
|
|||
|
||||
it('resolves the default demo root under the OS temp directory', () => {
|
||||
const dir = defaultDemoProjectDir();
|
||||
expect(dir.startsWith(join(tmpdir(), 'klo-demo-'))).toBe(true);
|
||||
expect(dir).toMatch(/klo-demo-[a-f0-9]{8}$/);
|
||||
expect(dir.startsWith(join(tmpdir(), 'ktx-demo-'))).toBe(true);
|
||||
expect(dir).toMatch(/ktx-demo-[a-f0-9]{8}$/);
|
||||
});
|
||||
|
||||
it('exports the packaged Orbit demo identity', () => {
|
||||
|
|
@ -124,7 +124,7 @@ describe('demo assets', () => {
|
|||
await expect(access(join(projectDir, 'raw-sources'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, '_schema'))).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('backend: anthropic');
|
||||
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
|
||||
expect(config).not.toContain('sk-ant-');
|
||||
|
|
@ -187,7 +187,7 @@ describe('demo assets', () => {
|
|||
await expect(inspectDemoProjectState(projectDir)).resolves.toEqual({
|
||||
status: 'missing',
|
||||
projectDir,
|
||||
missing: ['klo.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
|
||||
missing: ['ktx.yaml', 'demo.db', 'state.sqlite', 'replays/replay.memory-flow.v1.json'],
|
||||
});
|
||||
|
||||
await ensureDemoProject({ projectDir, force: false });
|
||||
|
|
@ -210,7 +210,7 @@ describe('demo assets', () => {
|
|||
await rm(join(projectDir, 'demo.db'), { force: true });
|
||||
|
||||
await expect(resetDemoProject({ projectDir, force: false })).rejects.toThrow(
|
||||
`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`,
|
||||
`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`,
|
||||
);
|
||||
|
||||
await expect(resetDemoProject({ projectDir, force: true })).resolves.toMatchObject({ projectDir });
|
||||
|
|
@ -218,10 +218,10 @@ describe('demo assets', () => {
|
|||
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
|
||||
});
|
||||
|
||||
it('preserves a user-edited klo.yaml across reset --force', async () => {
|
||||
it('preserves a user-edited ktx.yaml across reset --force', async () => {
|
||||
await ensureDemoProject({ projectDir, force: false });
|
||||
const customConfig = [
|
||||
'project: klo-demo-orbit',
|
||||
'project: ktx-demo-orbit',
|
||||
'connections:',
|
||||
` ${DEMO_CONNECTION_ID}:`,
|
||||
' driver: sqlite',
|
||||
|
|
@ -232,7 +232,7 @@ describe('demo assets', () => {
|
|||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' auto_commit: true',
|
||||
' author: klo <klo@example.com>',
|
||||
' author: ktx <ktx@example.com>',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: vertex',
|
||||
|
|
@ -253,20 +253,20 @@ describe('demo assets', () => {
|
|||
' failureMode: continue',
|
||||
'',
|
||||
].join('\n');
|
||||
await writeFile(join(projectDir, 'klo.yaml'), customConfig, 'utf-8');
|
||||
await writeFile(join(projectDir, 'ktx.yaml'), customConfig, 'utf-8');
|
||||
|
||||
await resetDemoProject({ projectDir, force: true });
|
||||
|
||||
const preserved = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const preserved = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(preserved).toBe(customConfig);
|
||||
expect(preserved).toContain('backend: vertex');
|
||||
expect(preserved).not.toContain('backend: anthropic');
|
||||
await expect(inspectDemoProjectState(projectDir)).resolves.toMatchObject({ status: 'ready' });
|
||||
});
|
||||
|
||||
it('still writes the default klo.yaml on reset when none exists', async () => {
|
||||
it('still writes the default ktx.yaml on reset when none exists', async () => {
|
||||
await resetDemoProject({ projectDir, force: true });
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('backend: anthropic');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
|
|||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { loadDemoReplayFile, loadLatestDemoReplay } from './demo-replay-store.js';
|
||||
|
||||
interface DemoProjectResult {
|
||||
|
|
@ -33,7 +33,7 @@ export const DEMO_REPLAY_FILE = 'replay.memory-flow.v1.json';
|
|||
export const DEMO_FULL_JOB_ID = 'demo-full-ingest';
|
||||
|
||||
const REQUIRED_BASE_PROJECT_PATHS = [
|
||||
'klo.yaml',
|
||||
'ktx.yaml',
|
||||
'demo.db',
|
||||
'state.sqlite',
|
||||
join('replays', DEMO_REPLAY_FILE),
|
||||
|
|
@ -70,7 +70,7 @@ async function exists(path: string): Promise<boolean> {
|
|||
|
||||
export function defaultDemoProjectDir(): string {
|
||||
const suffix = randomBytes(4).toString('hex');
|
||||
return join(tmpdir(), `klo-demo-${suffix}`);
|
||||
return join(tmpdir(), `ktx-demo-${suffix}`);
|
||||
}
|
||||
|
||||
export async function inspectDemoProjectState(projectDir: string): Promise<DemoProjectState> {
|
||||
|
|
@ -97,10 +97,10 @@ export async function inspectDemoProjectState(projectDir: string): Promise<DemoP
|
|||
export async function resetDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
|
||||
const projectDir = resolve(options.projectDir);
|
||||
if (!options.force) {
|
||||
throw new Error(`klo setup demo reset is destructive; pass --force to recreate ${projectDir}`);
|
||||
throw new Error(`ktx setup demo reset is destructive; pass --force to recreate ${projectDir}`);
|
||||
}
|
||||
|
||||
const preservedConfig = await readExistingConfig(join(projectDir, 'klo.yaml'));
|
||||
const preservedConfig = await readExistingConfig(join(projectDir, 'ktx.yaml'));
|
||||
const result = await ensureDemoProject({ projectDir, force: true });
|
||||
if (preservedConfig !== null) {
|
||||
await writeFile(result.configPath, preservedConfig, 'utf-8');
|
||||
|
|
@ -118,7 +118,7 @@ async function readExistingConfig(configPath: string): Promise<string | null> {
|
|||
|
||||
function demoConfig(databasePath: string): string {
|
||||
return [
|
||||
'project: klo-demo-orbit',
|
||||
'project: ktx-demo-orbit',
|
||||
'connections:',
|
||||
` ${DEMO_CONNECTION_ID}:`,
|
||||
' driver: sqlite',
|
||||
|
|
@ -129,7 +129,7 @@ function demoConfig(databasePath: string): string {
|
|||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' auto_commit: true',
|
||||
' author: klo <klo@example.com>',
|
||||
' author: ktx <ktx@example.com>',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
|
|
@ -185,7 +185,7 @@ async function assertPackagedSeededAssetsPresent(): Promise<void> {
|
|||
|
||||
export async function ensureDemoProject(options: EnsureDemoProjectOptions): Promise<DemoProjectResult> {
|
||||
const projectDir = resolve(options.projectDir);
|
||||
const configPath = join(projectDir, 'klo.yaml');
|
||||
const configPath = join(projectDir, 'ktx.yaml');
|
||||
if (!options.force && (await exists(configPath))) {
|
||||
throw new Error(`Demo project already exists at ${projectDir}; pass --force to recreate it`);
|
||||
}
|
||||
|
|
@ -237,7 +237,7 @@ export async function ensureSeededDemoProject(options: EnsureDemoProjectOptions)
|
|||
if (!options.force && error instanceof Error && error.message.includes('Demo project already exists')) {
|
||||
return {
|
||||
projectDir,
|
||||
configPath: join(projectDir, 'klo.yaml'),
|
||||
configPath: join(projectDir, 'ktx.yaml'),
|
||||
databasePath: join(projectDir, 'demo.db'),
|
||||
replayPath: join(projectDir, 'replays', DEMO_REPLAY_FILE),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@klo/context/ingest';
|
||||
import type { IngestReportSnapshot, LocalIngestResult, RunLocalIngestOptions } from '@ktx/context/ingest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
|
||||
import {
|
||||
|
|
@ -69,7 +69,7 @@ describe('full demo helpers', () => {
|
|||
let projectDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-'));
|
||||
projectDir = join(tempDir, 'demo');
|
||||
await ensureDemoProject({ projectDir, force: false });
|
||||
});
|
||||
|
|
@ -79,18 +79,18 @@ describe('full demo helpers', () => {
|
|||
});
|
||||
|
||||
it('fails full mode with exact Anthropic env guidance when the key is missing', async () => {
|
||||
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
|
||||
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
|
||||
|
||||
expect(() => assertFullDemoCredentials(project, {})).toThrow(
|
||||
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
|
||||
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
|
||||
);
|
||||
});
|
||||
|
||||
it('respects an existing gateway provider project for full mode', async () => {
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: klo-demo-orbit',
|
||||
'project: ktx-demo-orbit',
|
||||
'connections:',
|
||||
' orbit_demo:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -109,22 +109,22 @@ describe('full demo helpers', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
|
||||
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
|
||||
|
||||
expect(() => assertFullDemoCredentials(project, {})).not.toThrow();
|
||||
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'ready' });
|
||||
});
|
||||
|
||||
it('reports full-demo credential status without throwing', async () => {
|
||||
const project = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
|
||||
const project = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
|
||||
|
||||
expect(fullDemoCredentialStatus(project, {})).toEqual({ status: 'missing-anthropic-key' });
|
||||
expect(fullDemoCredentialStatus(project, { ANTHROPIC_API_KEY: 'sk-ant-test' })).toEqual({ status: 'ready' }); // pragma: allowlist secret
|
||||
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: klo-demo-orbit',
|
||||
'project: ktx-demo-orbit',
|
||||
'connections:',
|
||||
' orbit_demo:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -136,7 +136,7 @@ describe('full demo helpers', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const disabledProject = await import('@klo/context/project').then((mod) => mod.loadKloProject({ projectDir }));
|
||||
const disabledProject = await import('@ktx/context/project').then((mod) => mod.loadKtxProject({ projectDir }));
|
||||
expect(fullDemoCredentialStatus(disabledProject, {})).toEqual({ status: 'unsupported-provider', provider: 'none' });
|
||||
});
|
||||
|
||||
|
|
@ -192,9 +192,9 @@ describe('full demo helpers', () => {
|
|||
expect(summary).toContain('Full demo ingest: done');
|
||||
expect(summary).toContain('Saved memory: 1 wiki, 1 semantic layer');
|
||||
expect(summary).toContain('Provenance rows: 2');
|
||||
expect(summary).toContain('Next: klo setup demo inspect');
|
||||
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
|
||||
expect(summary).toContain('Next: klo setup demo replay');
|
||||
expect(summary).toContain('Next: ktx setup demo inspect');
|
||||
expect(summary).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
|
||||
expect(summary).toContain('Next: ktx setup demo replay');
|
||||
expect(summary).toContain('Replays the same visual story without calling the LLM again.');
|
||||
expect(summary).not.toContain('--viz');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { resolveKloConfigReference } from '@klo/context/core';
|
||||
import { resolveKtxConfigReference } from '@ktx/context/core';
|
||||
import {
|
||||
createMemoryFlowLiveBuffer,
|
||||
ingestReportToMemoryFlowReplay,
|
||||
|
|
@ -7,12 +7,12 @@ import {
|
|||
type LocalIngestResult,
|
||||
type MemoryFlowReplayInput,
|
||||
type RunLocalIngestOptions,
|
||||
} from '@klo/context/ingest';
|
||||
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
|
||||
import { runLocalScan, type LocalScanRunResult } from '@klo/context/scan';
|
||||
} from '@ktx/context/ingest';
|
||||
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||
import { runLocalScan, type LocalScanRunResult } from '@ktx/context/scan';
|
||||
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
|
||||
import { runDemoScan } from './demo-scan.js';
|
||||
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { formatNextStepLines } from './next-steps.js';
|
||||
|
||||
interface DemoFullOptions {
|
||||
|
|
@ -24,7 +24,7 @@ interface DemoFullOptions {
|
|||
}
|
||||
|
||||
export interface DemoFullResult {
|
||||
project: KloLocalProject;
|
||||
project: KtxLocalProject;
|
||||
scan: LocalScanRunResult;
|
||||
ingest: LocalIngestResult;
|
||||
report: IngestReportSnapshot;
|
||||
|
|
@ -54,7 +54,7 @@ function savedCounts(report: IngestReportSnapshot): { wikiCount: number; slCount
|
|||
}
|
||||
|
||||
export function fullDemoCredentialStatus(
|
||||
project: KloLocalProject,
|
||||
project: KtxLocalProject,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): FullDemoCredentialStatus {
|
||||
const llm = project.config.llm;
|
||||
|
|
@ -62,14 +62,14 @@ export function fullDemoCredentialStatus(
|
|||
return { status: 'unsupported-provider', provider: llm.provider.backend };
|
||||
}
|
||||
|
||||
if (llm.provider.backend === 'anthropic' && !resolveKloConfigReference(llm.provider.anthropic?.api_key, env)) {
|
||||
if (llm.provider.backend === 'anthropic' && !resolveKtxConfigReference(llm.provider.anthropic?.api_key, env)) {
|
||||
return { status: 'missing-anthropic-key' };
|
||||
}
|
||||
|
||||
return { status: 'ready' };
|
||||
}
|
||||
|
||||
export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.ProcessEnv = process.env): void {
|
||||
export function assertFullDemoCredentials(project: KtxLocalProject, env: NodeJS.ProcessEnv = process.env): void {
|
||||
const llm = project.config.llm;
|
||||
const status = fullDemoCredentialStatus(project, env);
|
||||
if (status.status === 'ready') {
|
||||
|
|
@ -78,13 +78,13 @@ export function assertFullDemoCredentials(project: KloLocalProject, env: NodeJS.
|
|||
|
||||
if (status.status === 'unsupported-provider') {
|
||||
throw new Error(
|
||||
'klo setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `klo setup demo init --force --no-input` to recreate the demo config, or run `klo setup demo --mode seeded --no-input` without credentials.',
|
||||
'ktx setup demo --mode full requires llm.provider.backend: anthropic, vertex, or gateway. Run `ktx setup demo init --force --no-input` to recreate the demo config, or run `ktx setup demo --mode seeded --no-input` without credentials.',
|
||||
);
|
||||
}
|
||||
|
||||
if (llm.provider.backend === 'anthropic') {
|
||||
throw new Error(
|
||||
'klo setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `klo setup demo --mode full --no-input`, or run `klo setup demo --mode seeded --no-input` without credentials.',
|
||||
'ktx setup demo --mode full needs ANTHROPIC_API_KEY. Export ANTHROPIC_API_KEY and rerun `ktx setup demo --mode full --no-input`, or run `ktx setup demo --mode seeded --no-input` without credentials.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ function initialFullReplay(projectDir: string): MemoryFlowReplayInput {
|
|||
|
||||
export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullResult> {
|
||||
await ensureDemoProjectForReuse(options.projectDir);
|
||||
const project = await loadKloProject({ projectDir: options.projectDir });
|
||||
const project = await loadKtxProject({ projectDir: options.projectDir });
|
||||
assertFullDemoCredentials(project, options.env);
|
||||
|
||||
const { result: scan } = await runDemoScan({
|
||||
|
|
@ -125,7 +125,7 @@ export async function runDemoFull(options: DemoFullOptions): Promise<DemoFullRes
|
|||
const executeLocalIngest = options.runLocalIngest ?? runLocalIngest;
|
||||
const ingest = await executeLocalIngest({
|
||||
project,
|
||||
adapters: createKloCliLocalIngestAdapters(project),
|
||||
adapters: createKtxCliLocalIngestAdapters(project),
|
||||
adapter: DEMO_ADAPTER,
|
||||
connectionId: DEMO_CONNECTION_ID,
|
||||
trigger: 'manual_resync',
|
||||
|
|
@ -152,9 +152,9 @@ export function formatFullDemoSummary(report: IngestReportSnapshot): string {
|
|||
`Sync: ${report.body.syncId}`,
|
||||
`Saved memory: ${counts.wikiCount} wiki, ${counts.slCount} semantic layer`,
|
||||
`Provenance rows: ${report.body.provenanceRows.length}`,
|
||||
'Next: klo setup demo inspect',
|
||||
' Shows the files, semantic-layer sources, and memory KLO just produced.',
|
||||
'Next: klo setup demo replay',
|
||||
'Next: ktx setup demo inspect',
|
||||
' Shows the files, semantic-layer sources, and memory KTX just produced.',
|
||||
'Next: ktx setup demo replay',
|
||||
' Replays the same visual story without calling the LLM again.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
|
@ -176,7 +176,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
|
|||
const conflictCount = report.body.conflictsResolved.length;
|
||||
const areasAnalyzed = workUnits.filter((wu) => wu.actions.length > 0).length;
|
||||
|
||||
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
|
||||
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
|
||||
|
||||
if (areasAnalyzed > 0) {
|
||||
lines.push(` ✓ Analyzed ${areasAnalyzed} business area${areasAnalyzed === 1 ? '' : 's'}`);
|
||||
|
|
@ -187,7 +187,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
|
|||
lines.push('');
|
||||
|
||||
if (counts.slCount > 0 || counts.wikiCount > 0) {
|
||||
lines.push(' KLO created:');
|
||||
lines.push(' KTX created:');
|
||||
if (counts.slCount > 0) lines.push(` 📊 ${counts.slCount} query definition${counts.slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
|
||||
if (counts.wikiCount > 0) lines.push(` 📝 ${counts.wikiCount} knowledge page${counts.wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
|
||||
lines.push('');
|
||||
|
|
@ -206,7 +206,7 @@ export function formatCleanDemoSummary(report: IngestReportSnapshot, projectDir:
|
|||
lines.push(' What to do next:');
|
||||
lines.push(...formatNextStepLines());
|
||||
lines.push('');
|
||||
lines.push(` Your KLO project files are at: ${projectDir}`);
|
||||
lines.push(` Your KTX project files are at: ${projectDir}`);
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe('demo interaction decisions', () => {
|
|||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-interaction-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-interaction-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -53,7 +53,7 @@ describe('demo interaction decisions', () => {
|
|||
prompts: createTestDemoPromptAdapter({ choices: [] }),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`Demo project is not ready at ${tempDir}: missing demo.db. Run klo setup demo reset --project-dir ${tempDir} --force --no-input`,
|
||||
`Demo project is not ready at ${tempDir}: missing demo.db. Run ktx setup demo reset --project-dir ${tempDir} --force --no-input`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { cancel, confirm, isCancel, password, select, text } from '@clack/prompt
|
|||
import type { Option as ClackOption } from '@clack/prompts';
|
||||
import { resolve } from 'node:path';
|
||||
import { inspectDemoProjectState } from './demo-assets.js';
|
||||
import type { KloDemoInputMode } from './demo.js';
|
||||
import type { KtxDemoInputMode } from './demo.js';
|
||||
import { withMenuOptionsSpacing } from './prompt-navigation.js';
|
||||
|
||||
type DemoPromptOption<T extends string> = ClackOption<T>;
|
||||
|
|
@ -29,7 +29,7 @@ type FullCredentialDecision =
|
|||
| { action: 'run-mode'; mode: 'seeded' | 'replay' }
|
||||
| { action: 'cancel' };
|
||||
|
||||
function isInteractive(inputMode: KloDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
|
||||
function isInteractive(inputMode: KtxDemoInputMode | undefined, io: DemoInteractiveIo): boolean {
|
||||
return inputMode !== 'disabled' && io.stdin?.isTTY === true && io.stdout.isTTY === true;
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export function createTestDemoPromptAdapter(options: {
|
|||
|
||||
export async function chooseDemoProjectForInteractiveRun(options: {
|
||||
projectDir: string;
|
||||
inputMode?: KloDemoInputMode;
|
||||
inputMode?: KtxDemoInputMode;
|
||||
io: DemoInteractiveIo;
|
||||
prompts?: DemoPromptAdapter;
|
||||
}): Promise<DemoProjectDecision> {
|
||||
|
|
@ -108,7 +108,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
|
|||
if (!isInteractive(options.inputMode, options.io)) {
|
||||
if (state.status === 'corrupt') {
|
||||
throw new Error(
|
||||
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run klo setup demo reset --project-dir ${projectDir} --force --no-input`,
|
||||
`Demo project is not ready at ${projectDir}: missing ${state.missing.join(', ')}. Run ktx setup demo reset --project-dir ${projectDir} --force --no-input`,
|
||||
);
|
||||
}
|
||||
return { action: 'use', projectDir, reset: false };
|
||||
|
|
@ -163,7 +163,7 @@ export async function chooseDemoProjectForInteractiveRun(options: {
|
|||
|
||||
export async function resolveFullCredentialDecision(options: {
|
||||
needsAnthropicKey: boolean;
|
||||
inputMode?: KloDemoInputMode;
|
||||
inputMode?: KtxDemoInputMode;
|
||||
io: DemoInteractiveIo;
|
||||
env: NodeJS.ProcessEnv;
|
||||
prompts?: DemoPromptAdapter;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildDemoMetrics,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
|
||||
const DEFAULT_INPUT_TOKENS_PER_STEP = 4500;
|
||||
const DEFAULT_OUTPUT_TOKENS_PER_STEP = 700;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createPlainProgressEmitter, formatMemoryFlowEventLine } from './demo-progress.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { KloDemoIo } from './demo.js';
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import type { KtxDemoIo } from './demo.js';
|
||||
|
||||
function plural(n: number, one: string, many = `${one}s`): string {
|
||||
return `${n} ${n === 1 ? one : many}`;
|
||||
|
|
@ -62,7 +62,7 @@ export function formatMemoryFlowEventLine(event: MemoryFlowEvent): string | null
|
|||
}
|
||||
}
|
||||
|
||||
export function createPlainProgressEmitter(io: KloDemoIo): (snapshot: MemoryFlowReplayInput) => void {
|
||||
export function createPlainProgressEmitter(io: KtxDemoIo): (snapshot: MemoryFlowReplayInput) => void {
|
||||
let printed = 0;
|
||||
return (snapshot) => {
|
||||
while (printed < snapshot.events.length) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdtemp, readFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import { type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay, writeDemoReplay } from './demo-replay-store.js';
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ function replay(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowRepla
|
|||
|
||||
describe('demo replay store', () => {
|
||||
it('writes a versioned replay file and updates latest', async () => {
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-'));
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-'));
|
||||
|
||||
const saved = await writeDemoReplay(projectDir, replay(), { label: 'full' });
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe('demo replay store', () => {
|
|||
});
|
||||
|
||||
it('returns null when no latest local replay exists', async () => {
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'klo-demo-replay-store-empty-'));
|
||||
const projectDir = await mkdtemp(join(tmpdir(), 'ktx-demo-replay-store-empty-'));
|
||||
|
||||
await expect(loadLatestDemoReplay(projectDir)).resolves.toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { constants as fsConstants } from 'node:fs';
|
||||
import { access, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import { parseMemoryFlowReplayInput, type MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
|
||||
interface StoredMemoryFlowReplayFile {
|
||||
memoryFlowReplaySchemaVersion: 1;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest';
|
|||
import { findLatestDemoScanReport, runDemoScan } from './demo-scan.js';
|
||||
|
||||
describe('demo scan helpers', () => {
|
||||
const projectDir = join(tmpdir(), `klo-demo-scan-${process.pid}`);
|
||||
const projectDir = join(tmpdir(), `ktx-demo-scan-${process.pid}`);
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@klo/context/ingest';
|
||||
import { loadKloProject, type KloLocalProject } from '@klo/context/project';
|
||||
import { runLocalScan, type KloScanReport, type LocalScanRunResult } from '@klo/context/scan';
|
||||
import { getLocalIngestStatus, type IngestReportSnapshot, type MemoryFlowReplayInput } from '@ktx/context/ingest';
|
||||
import { loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||
import { runLocalScan, type KtxScanReport, type LocalScanRunResult } from '@ktx/context/scan';
|
||||
import { DEMO_ADAPTER, DEMO_CONNECTION_ID, DEMO_FULL_JOB_ID, ensureDemoProject } from './demo-assets.js';
|
||||
import { loadLatestDemoReplay } from './demo-replay-store.js';
|
||||
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
|
||||
|
||||
interface DemoScanOptions {
|
||||
projectDir: string;
|
||||
|
|
@ -13,13 +13,13 @@ interface DemoScanOptions {
|
|||
}
|
||||
|
||||
interface DemoScanResult {
|
||||
project: KloLocalProject;
|
||||
project: KtxLocalProject;
|
||||
result: LocalScanRunResult;
|
||||
}
|
||||
|
||||
interface DemoInspectSummary {
|
||||
projectDir: string;
|
||||
scanReport: KloScanReport | null;
|
||||
scanReport: KtxScanReport | null;
|
||||
fullReport: IngestReportSnapshot | null;
|
||||
semanticLayerFileCount: number;
|
||||
knowledgeFileCount: number;
|
||||
|
|
@ -28,7 +28,7 @@ interface DemoInspectSummary {
|
|||
}
|
||||
|
||||
interface DemoInspectDeps {
|
||||
findFullReport?: (project: KloLocalProject) => Promise<IngestReportSnapshot | null>;
|
||||
findFullReport?: (project: KtxLocalProject) => Promise<IngestReportSnapshot | null>;
|
||||
}
|
||||
|
||||
async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
|
||||
|
|
@ -40,36 +40,36 @@ async function ensureDemoProjectForReuse(projectDir: string): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function loadReadyDemoProject(projectDir: string): Promise<KloLocalProject> {
|
||||
async function loadReadyDemoProject(projectDir: string): Promise<KtxLocalProject> {
|
||||
try {
|
||||
return await loadKloProject({ projectDir });
|
||||
return await loadKtxProject({ projectDir });
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Demo project is not ready at ${projectDir}: ${reason}. Run klo setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
|
||||
`Demo project is not ready at ${projectDir}: ${reason}. Run ktx setup demo init --project-dir ${projectDir} --force --no-input to recreate it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function reportDiff(report: KloScanReport): string {
|
||||
function reportDiff(report: KtxScanReport): string {
|
||||
return `+${report.diffSummary.tablesAdded}/~${report.diffSummary.tablesModified}/-${report.diffSummary.tablesDeleted}/=${report.diffSummary.tablesUnchanged}`;
|
||||
}
|
||||
|
||||
function jsonReport(raw: string, path: string): KloScanReport {
|
||||
function jsonReport(raw: string, path: string): KtxScanReport {
|
||||
try {
|
||||
return JSON.parse(raw) as KloScanReport;
|
||||
return JSON.parse(raw) as KtxScanReport;
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Invalid demo scan report at ${path}: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function countFiles(project: KloLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
|
||||
async function countFiles(project: KtxLocalProject, root: string, predicate: (path: string) => boolean): Promise<number> {
|
||||
const { files } = await project.fileStore.listFiles(root, true);
|
||||
return files.filter(predicate).length;
|
||||
}
|
||||
|
||||
async function findFullDemoReport(project: KloLocalProject): Promise<IngestReportSnapshot | null> {
|
||||
async function findFullDemoReport(project: KtxLocalProject): Promise<IngestReportSnapshot | null> {
|
||||
return getLocalIngestStatus(project, DEMO_FULL_JOB_ID);
|
||||
}
|
||||
|
||||
|
|
@ -92,13 +92,13 @@ export async function runDemoScan(options: DemoScanOptions): Promise<DemoScanRes
|
|||
trigger: 'cli',
|
||||
jobId: options.jobId ?? 'demo-scan',
|
||||
now: options.now,
|
||||
adapters: createKloCliLocalIngestAdapters(project),
|
||||
adapters: createKtxCliLocalIngestAdapters(project),
|
||||
});
|
||||
|
||||
return { project, result };
|
||||
}
|
||||
|
||||
export async function findLatestDemoScanReport(projectDir: string): Promise<KloScanReport | null> {
|
||||
export async function findLatestDemoScanReport(projectDir: string): Promise<KtxScanReport | null> {
|
||||
const project = await loadReadyDemoProject(projectDir);
|
||||
const root = `raw-sources/${DEMO_CONNECTION_ID}/${DEMO_ADAPTER}`;
|
||||
const { files } = await project.fileStore.listFiles(root, true);
|
||||
|
|
@ -117,7 +117,7 @@ export async function findLatestDemoScanReport(projectDir: string): Promise<KloS
|
|||
|
||||
export async function inspectDemoProject(
|
||||
projectDir: string,
|
||||
projectOverride?: KloLocalProject,
|
||||
projectOverride?: KtxLocalProject,
|
||||
deps: DemoInspectDeps = {},
|
||||
): Promise<DemoInspectSummary> {
|
||||
const project = projectOverride ?? (await loadReadyDemoProject(projectDir));
|
||||
|
|
@ -143,7 +143,7 @@ export async function inspectDemoProject(
|
|||
};
|
||||
}
|
||||
|
||||
export function formatDemoScanSummary(report: KloScanReport): string {
|
||||
export function formatDemoScanSummary(report: KtxScanReport): string {
|
||||
return [
|
||||
'Demo scan: done',
|
||||
`Connection: ${report.connectionId}`,
|
||||
|
|
@ -152,7 +152,7 @@ export function formatDemoScanSummary(report: KloScanReport): string {
|
|||
`Tables: ${reportDiff(report)}`,
|
||||
`Semantic-layer artifacts: ${report.artifactPaths.manifestShards.length}`,
|
||||
`Report: ${report.artifactPaths.reportPath ?? 'none'}`,
|
||||
'Next: klo setup demo inspect',
|
||||
'Next: ktx setup demo inspect',
|
||||
' Shows the files and semantic-layer draft created from the database scan.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
|
@ -190,22 +190,22 @@ export function formatDemoInspect(summary: DemoInspectSummary): string {
|
|||
: [report ? 'Memory synthesis: full mode not run' : 'Memory synthesis: not run'];
|
||||
const next = fullReport
|
||||
? [
|
||||
`Next: klo ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
|
||||
`Next: ktx ingest watch ${fullReport.runId} --project-dir ${summary.projectDir}`,
|
||||
' Opens the captured run timeline and lets you inspect what happened.',
|
||||
'Next: klo setup demo replay',
|
||||
'Next: ktx setup demo replay',
|
||||
' Replays the same visual story without calling the LLM again.',
|
||||
]
|
||||
: report
|
||||
? [
|
||||
'Next: klo setup demo --mode full',
|
||||
'Next: ktx setup demo --mode full',
|
||||
' Runs the full AI-backed pass with your LLM provider.',
|
||||
'Next: klo setup demo replay',
|
||||
'Next: ktx setup demo replay',
|
||||
' Replays the packaged visual story without calling the LLM.',
|
||||
]
|
||||
: [
|
||||
'Next: klo setup demo --no-input',
|
||||
'Next: ktx setup demo --no-input',
|
||||
' Runs the pre-seeded demo without calling the LLM.',
|
||||
'Next: klo setup demo --mode full',
|
||||
'Next: ktx setup demo --mode full',
|
||||
' Runs the full AI-backed pass with your LLM provider.',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { join } from 'node:path';
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { runDemoSeeded } from './demo-seeded.js';
|
||||
import { formatSeededInspect, inspectSeededProject } from './demo-seeded-inspect.js';
|
||||
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
|
||||
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
|
||||
|
||||
describe('seeded demo inspect contract', () => {
|
||||
const projectDir = join(tmpdir(), `klo-demo-seeded-inspect-${process.pid}`);
|
||||
const projectDir = join(tmpdir(), `ktx-demo-seeded-inspect-${process.pid}`);
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
|
|
@ -59,7 +59,7 @@ describe('seeded demo inspect contract', () => {
|
|||
reports: { primaryPath: 'reports/seeded-demo-report.json', fileCount: 1 },
|
||||
replays: { primaryPath: 'replays/replay.memory-flow.v1.json', latestPath: 'replays/latest.memory-flow.v1.json' },
|
||||
},
|
||||
nextCommands: KLO_NEXT_STEP_COMMANDS,
|
||||
nextCommands: KTX_NEXT_STEP_COMMANDS,
|
||||
});
|
||||
|
||||
expect(inspect.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
|
||||
|
|
@ -89,13 +89,13 @@ describe('seeded demo inspect contract', () => {
|
|||
expect(output).toContain('Report: reports/seeded-demo-report.json');
|
||||
expect(output).toContain('Replay: replays/replay.memory-flow.v1.json');
|
||||
expect(output).toContain('Latest replay: seeded (packaged, prebuilt)');
|
||||
expect(output).toContain(' $ klo agent tools --json');
|
||||
expect(output).toContain(' $ klo agent context --json');
|
||||
expect(output).toContain(' $ klo serve --mcp stdio --user-id local');
|
||||
expect(output.indexOf('klo agent tools --json')).toBeLessThan(
|
||||
output.indexOf('klo serve --mcp stdio --user-id local'),
|
||||
expect(output).toContain(' $ ktx agent tools --json');
|
||||
expect(output).toContain(' $ ktx agent context --json');
|
||||
expect(output).toContain(' $ ktx serve --mcp stdio --user-id local');
|
||||
expect(output.indexOf('ktx agent tools --json')).toBeLessThan(
|
||||
output.indexOf('ktx serve --mcp stdio --user-id local'),
|
||||
);
|
||||
expect(output).not.toContain('klo ask');
|
||||
expect(output).not.toContain('ktx ask');
|
||||
expect(output).not.toContain('deterministic mode');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { constants as fsConstants } from 'node:fs';
|
||||
import { access, readFile, readdir } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { loadPackagedDemoReplay } from './demo-assets.js';
|
||||
import { DEMO_LATEST_REPLAY_FILE, loadLatestDemoReplay } from './demo-replay-store.js';
|
||||
import { KLO_NEXT_STEP_COMMANDS, KLO_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
|
||||
import { KTX_NEXT_STEP_COMMANDS, KTX_NEXT_STEP_COMMAND_WIDTH } from './next-steps.js';
|
||||
|
||||
type SeededInspectReadiness = 'missing' | 'ready' | 'corrupt';
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export interface SeededInspectSummary {
|
|||
}
|
||||
|
||||
const REQUIRED_SEEDED_PROJECT_PATHS = [
|
||||
'klo.yaml',
|
||||
'ktx.yaml',
|
||||
'demo.db',
|
||||
'state.sqlite',
|
||||
'manifest.json',
|
||||
|
|
@ -181,7 +181,7 @@ function sourceBundleFromManifest(manifest: DemoSeededManifest): SeededInspectSu
|
|||
}
|
||||
|
||||
function nextCommands(): SeededInspectSummary['nextCommands'] {
|
||||
return [...KLO_NEXT_STEP_COMMANDS];
|
||||
return [...KTX_NEXT_STEP_COMMANDS];
|
||||
}
|
||||
|
||||
function modeMetadataFromReplay(replay: MemoryFlowReplayInput | null): SeededInspectSummary['modeMetadata'] {
|
||||
|
|
@ -291,9 +291,9 @@ export function formatSeededInspect(summary: SeededInspectSummary): string {
|
|||
);
|
||||
|
||||
for (const command of summary.nextCommands) {
|
||||
lines.push(` $ ${command.command.padEnd(KLO_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
|
||||
lines.push(` $ ${command.command.padEnd(KTX_NEXT_STEP_COMMAND_WIDTH)} ${command.description}`);
|
||||
}
|
||||
|
||||
lines.push('', `Your KLO project files are at: ${summary.projectDir}`, '');
|
||||
lines.push('', `Your KTX project files are at: ${summary.projectDir}`, '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ensureSeededDemoProject } from './demo-assets.js';
|
|||
import { runDemoSeeded } from './demo-seeded.js';
|
||||
|
||||
describe('demo seeded mode', () => {
|
||||
const projectDir = join(tmpdir(), `klo-demo-seeded-${process.pid}`);
|
||||
const projectDir = join(tmpdir(), `ktx-demo-seeded-${process.pid}`);
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(projectDir, { recursive: true, force: true });
|
||||
|
|
@ -17,7 +17,7 @@ describe('demo seeded mode', () => {
|
|||
|
||||
expect(result.projectDir).toBe(projectDir);
|
||||
await expect(access(join(projectDir, 'demo.db'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, 'klo.yaml'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, 'ktx.yaml'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, 'manifest.json'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, 'semantic-layer/orbit_demo/accounts.yaml'))).resolves.toBeUndefined();
|
||||
await expect(access(join(projectDir, 'knowledge/global/arr-contract-first.md'))).resolves.toBeUndefined();
|
||||
|
|
@ -35,7 +35,7 @@ describe('demo seeded mode', () => {
|
|||
expect(result.replay.metadata?.timing).toBe('prebuilt');
|
||||
expect(result.inspect.mode).toBe('seeded');
|
||||
|
||||
const config = await readFile(join(projectDir, 'klo.yaml'), 'utf-8');
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf-8');
|
||||
expect(config).toContain('api_key: env:ANTHROPIC_API_KEY');
|
||||
expect(config).not.toContain('sk-ant-');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import {
|
||||
ensureSeededDemoProject,
|
||||
loadPackagedDemoReplay,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@klo/context/ingest';
|
||||
import type { IngestReportSnapshot, MemoryFlowReplayInput } from '@ktx/context/ingest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runKloDemo } from './demo.js';
|
||||
import { runKtxDemo } from './demo.js';
|
||||
import { DEMO_FULL_JOB_ID, defaultDemoProjectDir, ensureDemoProject } from './demo-assets.js';
|
||||
import type { DemoFullResult } from './demo-full.js';
|
||||
import { createTestDemoPromptAdapter } from './demo-interaction.js';
|
||||
import type { renderMemoryFlowTui } from './memory-flow-tui.js';
|
||||
import { KLO_NEXT_STEP_COMMANDS } from './next-steps.js';
|
||||
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
|
||||
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
|
||||
|
||||
function makeIo(options: { isTTY?: boolean; columns?: number; rawMode?: boolean } = {}) {
|
||||
|
|
@ -108,12 +108,12 @@ function fakeFullResult(projectDir: string): DemoFullResult {
|
|||
};
|
||||
}
|
||||
|
||||
describe('runKloDemo', () => {
|
||||
describe('runKtxDemo', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
resetVizFallbackWarningsForTest();
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-command-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-command-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -123,7 +123,7 @@ describe('runKloDemo', () => {
|
|||
it('initializes the demo project', async () => {
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
|
||||
runKtxDemo({ command: 'init', projectDir: tempDir, force: false, inputMode: 'disabled' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain(`Demo project: ${tempDir}`);
|
||||
|
|
@ -135,14 +135,14 @@ describe('runKloDemo', () => {
|
|||
it('renders the packaged replay in no-input viz mode', async () => {
|
||||
const io = makeIo({ isTTY: true });
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'xterm-256color' } },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow Warehouse + dbt + BI + Docs done');
|
||||
expect(io.stdout()).toContain('KTX memory flow Warehouse + dbt + BI + Docs done');
|
||||
expect(io.stdout()).toContain('Saved 16 memories');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -152,7 +152,7 @@ describe('runKloDemo', () => {
|
|||
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
|
||||
|
|
@ -166,7 +166,7 @@ describe('runKloDemo', () => {
|
|||
adapter: 'live-database',
|
||||
});
|
||||
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
|
||||
expect(io.stdout()).toContain('KLO finished ingesting your data');
|
||||
expect(io.stdout()).toContain('KTX finished ingesting your data');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ describe('runKloDemo', () => {
|
|||
const renderStoredMemoryFlow = vi.fn<typeof renderMemoryFlowTui>(async () => true);
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
|
||||
|
|
@ -184,7 +184,7 @@ describe('runKloDemo', () => {
|
|||
|
||||
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
|
||||
expect(renderStoredMemoryFlow.mock.calls[0]?.[2]).toEqual({ speedMultiplier: 0.125 });
|
||||
expect(io.stdout()).toContain('KLO finished ingesting your data');
|
||||
expect(io.stdout()).toContain('KTX finished ingesting your data');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ describe('runKloDemo', () => {
|
|||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'replay', projectDir: tempDir, outputMode: 'viz' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'xterm-256color' }, renderStoredMemoryFlow },
|
||||
|
|
@ -203,10 +203,10 @@ describe('runKloDemo', () => {
|
|||
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('Memory-flow summary: done');
|
||||
expect(io.stdout()).toContain('Connection: orbit_demo');
|
||||
expect(io.stdout()).toContain('klo sl list');
|
||||
expect(io.stdout()).toContain('klo wiki list');
|
||||
expect(io.stdout()).toContain('klo serve --mcp stdio --user-id local');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).toContain('ktx sl list');
|
||||
expect(io.stdout()).toContain('ktx wiki list');
|
||||
expect(io.stdout()).toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -216,15 +216,15 @@ describe('runKloDemo', () => {
|
|||
const testIo = makeIo({ isTTY: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
|
||||
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Memory-flow summary: done');
|
||||
expect(testIo.stdout()).toContain('Connection: orbit_demo');
|
||||
expect(testIo.stdout()).toContain('klo sl list');
|
||||
expect(testIo.stdout()).toContain('klo wiki list');
|
||||
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
|
||||
expect(testIo.stdout()).not.toContain('KLO memory flow');
|
||||
expect(testIo.stdout()).toContain('ktx sl list');
|
||||
expect(testIo.stdout()).toContain('ktx wiki list');
|
||||
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(testIo.stdout()).not.toContain('KTX memory flow');
|
||||
expect(testIo.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -233,7 +233,7 @@ describe('runKloDemo', () => {
|
|||
it('prints JSON replay output when requested', async () => {
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
|
||||
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(JSON.parse(io.stdout())).toMatchObject({ runId: 'demo-seeded-orbit', connectionId: 'orbit_demo' });
|
||||
|
|
@ -242,7 +242,7 @@ describe('runKloDemo', () => {
|
|||
|
||||
it('runs the packaged SQLite demo scan', async () => {
|
||||
const io = makeIo();
|
||||
await expect(runKloDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
|
||||
await expect(runKtxDemo({ command: 'scan', projectDir: tempDir, inputMode: 'disabled' }, io.io)).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Demo scan: done');
|
||||
expect(io.stdout()).toContain('Connection: orbit_demo');
|
||||
|
|
@ -254,7 +254,7 @@ describe('runKloDemo', () => {
|
|||
it('runs seeded mode with pre-seeded assets and inspect summary', async () => {
|
||||
const io = makeIo({ isTTY: true });
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'xterm-256color' } },
|
||||
|
|
@ -272,7 +272,7 @@ describe('runKloDemo', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'seeded', projectDir: defaultDemoProjectDir(), outputMode: 'plain', inputMode: 'disabled' },
|
||||
io.io,
|
||||
),
|
||||
|
|
@ -282,10 +282,10 @@ describe('runKloDemo', () => {
|
|||
expect(io.stdout()).toContain('Source: packaged demo project');
|
||||
expect(io.stdout()).toContain('Generated context: prebuilt from bundled assets');
|
||||
expect(io.stdout()).toContain('LLM calls: none');
|
||||
expect(io.stdout()).toContain('Your KLO project files are at:');
|
||||
expect(io.stdout()).toContain(join(tmpdir(), 'klo-demo-'));
|
||||
expect(io.stdout()).toContain('klo serve --mcp stdio');
|
||||
expect(io.stdout()).not.toContain(['klo', 'mcp'].join(' '));
|
||||
expect(io.stdout()).toContain('Your KTX project files are at:');
|
||||
expect(io.stdout()).toContain(join(tmpdir(), 'ktx-demo-'));
|
||||
expect(io.stdout()).toContain('ktx serve --mcp stdio');
|
||||
expect(io.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
|
||||
expect(io.stdout()).not.toContain('deterministic');
|
||||
});
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ describe('runKloDemo', () => {
|
|||
const testIo = makeIo({ isTTY: true, columns: 120 });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'seeded', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{ env: { ...process.env, TERM: 'dumb' } },
|
||||
|
|
@ -310,19 +310,19 @@ describe('runKloDemo', () => {
|
|||
it('prints demo inspect as plain text and JSON', async () => {
|
||||
const seededIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
|
||||
runKtxDemo({ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, seededIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const plainIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
|
||||
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, plainIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(plainIo.stdout()).toContain('Mode: seeded');
|
||||
expect(plainIo.stdout()).toContain('Semantic-layer sources:');
|
||||
|
||||
const jsonIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
|
||||
runKtxDemo({ command: 'inspect', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, jsonIo.io),
|
||||
).resolves.toBe(0);
|
||||
const parsed = JSON.parse(jsonIo.stdout());
|
||||
expect(parsed).toMatchObject({
|
||||
|
|
@ -347,7 +347,7 @@ describe('runKloDemo', () => {
|
|||
generatedContext: 'prebuilt from bundled assets',
|
||||
llmCalls: 'none',
|
||||
},
|
||||
nextCommands: KLO_NEXT_STEP_COMMANDS,
|
||||
nextCommands: KTX_NEXT_STEP_COMMANDS,
|
||||
});
|
||||
expect(parsed.generatedOutputs.replays.fileCount).toBeGreaterThanOrEqual(3);
|
||||
expect(jsonIo.stderr()).toBe('');
|
||||
|
|
@ -359,7 +359,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
|
||||
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz', inputMode: 'disabled' }, testIo.io, {
|
||||
env: {},
|
||||
runFullDemo,
|
||||
}),
|
||||
|
|
@ -372,10 +372,10 @@ describe('runKloDemo', () => {
|
|||
onMemoryFlowChange: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(testIo.stdout()).toContain('KLO memory flow orbit_demo/live-database done');
|
||||
expect(testIo.stdout()).toContain('KTX memory flow orbit_demo/live-database done');
|
||||
expect(testIo.stdout()).toContain('Full demo ingest: done');
|
||||
expect(testIo.stdout()).toContain('Next: klo setup demo inspect');
|
||||
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KLO just produced.');
|
||||
expect(testIo.stdout()).toContain('Next: ktx setup demo inspect');
|
||||
expect(testIo.stdout()).toContain('Shows the files, semantic-layer sources, and memory KTX just produced.');
|
||||
});
|
||||
|
||||
it('streams live memory-flow snapshots for full demo viz and then prints final summary', async () => {
|
||||
|
|
@ -399,7 +399,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
|
||||
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
|
||||
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
||||
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
|
||||
runFullDemo,
|
||||
|
|
@ -411,12 +411,12 @@ describe('runKloDemo', () => {
|
|||
expect(liveSession.update).toHaveBeenCalledTimes(1);
|
||||
expect(liveSession.close).toHaveBeenCalledTimes(1);
|
||||
expect(testIo.stdout()).not.toContain('Memory-flow summary: done');
|
||||
expect(testIo.stdout()).toContain('KLO finished ingesting your data');
|
||||
expect(testIo.stdout()).toContain('klo sl list');
|
||||
expect(testIo.stdout()).toContain('klo wiki list');
|
||||
expect(testIo.stdout()).toContain('klo serve --mcp stdio --user-id local');
|
||||
expect(testIo.stdout()).not.toContain(['klo', 'ask'].join(' '));
|
||||
expect(testIo.stdout()).not.toContain(['klo', 'mcp'].join(' '));
|
||||
expect(testIo.stdout()).toContain('KTX finished ingesting your data');
|
||||
expect(testIo.stdout()).toContain('ktx sl list');
|
||||
expect(testIo.stdout()).toContain('ktx wiki list');
|
||||
expect(testIo.stdout()).toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(testIo.stdout()).not.toContain(['ktx', 'ask'].join(' '));
|
||||
expect(testIo.stdout()).not.toContain(['ktx', 'mcp'].join(' '));
|
||||
});
|
||||
|
||||
it('uses plain progress for full demo viz when stdin raw mode is unavailable', async () => {
|
||||
|
|
@ -440,7 +440,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
|
||||
runKtxDemo({ command: 'full', projectDir: tempDir, outputMode: 'viz' }, testIo.io, {
|
||||
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
||||
prompts: createTestDemoPromptAdapter({ choices: ['reuse'] }),
|
||||
runFullDemo,
|
||||
|
|
@ -456,7 +456,7 @@ describe('runKloDemo', () => {
|
|||
);
|
||||
expect(testIo.stdout()).toContain('[connect] Connected live-database - 7 database files (demo_full)');
|
||||
expect(testIo.stdout()).toContain('Full demo ingest: done');
|
||||
expect(testIo.stdout()).not.toContain('KLO memory flow');
|
||||
expect(testIo.stdout()).not.toContain('KTX memory flow');
|
||||
expect(testIo.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -486,7 +486,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
|
||||
|
|
@ -510,7 +510,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, runFullDemo }, // pragma: allowlist secret
|
||||
|
|
@ -526,7 +526,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'ingest', mode: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{ env: {}, runFullDemo },
|
||||
|
|
@ -537,12 +537,12 @@ describe('runKloDemo', () => {
|
|||
});
|
||||
|
||||
it('saves full-demo replay output for the next demo replay command', async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'klo-demo-full-replay-'));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-demo-full-replay-'));
|
||||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
io.io,
|
||||
{
|
||||
|
|
@ -554,7 +554,7 @@ describe('runKloDemo', () => {
|
|||
|
||||
const replayIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
|
||||
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' }, replayIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(JSON.parse(replayIo.stdout())).toMatchObject({
|
||||
runId: 'run-full',
|
||||
|
|
@ -566,7 +566,7 @@ describe('runKloDemo', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'ingest', mode: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
),
|
||||
|
|
@ -581,7 +581,7 @@ describe('runKloDemo', () => {
|
|||
const runDoctor = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{
|
||||
command: 'doctor',
|
||||
projectDir: tempDir,
|
||||
|
|
@ -610,13 +610,13 @@ describe('runKloDemo', () => {
|
|||
|
||||
const rejected = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
|
||||
runKtxDemo({ command: 'reset', projectDir: tempDir, force: false, inputMode: 'disabled' }, rejected.io),
|
||||
).resolves.toBe(1);
|
||||
expect(rejected.stderr()).toContain(`klo setup demo reset is destructive; pass --force to recreate ${tempDir}`);
|
||||
expect(rejected.stderr()).toContain(`ktx setup demo reset is destructive; pass --force to recreate ${tempDir}`);
|
||||
|
||||
const accepted = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
|
||||
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, accepted.io),
|
||||
).resolves.toBe(0);
|
||||
expect(accepted.stdout()).toContain(`Demo project reset: ${tempDir}`);
|
||||
});
|
||||
|
|
@ -624,12 +624,12 @@ describe('runKloDemo', () => {
|
|||
it('rehydrates seeded assets after reset --force', async () => {
|
||||
const resetIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
|
||||
runKtxDemo({ command: 'reset', projectDir: tempDir, force: true, inputMode: 'disabled' }, resetIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const seededIo = makeIo();
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'seeded', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
seededIo.io,
|
||||
),
|
||||
|
|
@ -648,11 +648,11 @@ describe('runKloDemo', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
|
||||
runKtxDemo({ command: 'replay', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' }, testIo.io),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toContain(`Demo project is not ready at ${tempDir}: missing demo.db`);
|
||||
expect(testIo.stderr()).toContain(`klo setup demo reset --project-dir ${tempDir} --force --no-input`);
|
||||
expect(testIo.stderr()).toContain(`ktx setup demo reset --project-dir ${tempDir} --force --no-input`);
|
||||
});
|
||||
|
||||
it('uses a process-local Anthropic key from the interactive prompt', async () => {
|
||||
|
|
@ -661,7 +661,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -682,7 +682,7 @@ describe('runKloDemo', () => {
|
|||
onMemoryFlowChange: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
expect(await readFile(join(tempDir, 'klo.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
|
||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('api_key: env:ANTHROPIC_API_KEY');
|
||||
});
|
||||
|
||||
it('routes an interactive missing-key choice to seeded mode', async () => {
|
||||
|
|
@ -691,7 +691,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -712,7 +712,7 @@ describe('runKloDemo', () => {
|
|||
const testIo = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'plain' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -733,7 +733,7 @@ describe('runKloDemo', () => {
|
|||
await ensureDemoProject({ projectDir: tempDir, force: false });
|
||||
|
||||
await expect(
|
||||
runKloDemo(
|
||||
runKtxDemo(
|
||||
{ command: 'full', projectDir: tempDir, outputMode: 'viz' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -745,7 +745,7 @@ describe('runKloDemo', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(runFullDemo).not.toHaveBeenCalled();
|
||||
expect(testIo.stdout()).toContain('KLO memory flow');
|
||||
expect(testIo.stdout()).toContain('KTX memory flow');
|
||||
expect(testIo.stdout()).toContain('done');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import {
|
|||
formatMemoryFlowFinalSummary,
|
||||
renderMemoryFlowReplay,
|
||||
type MemoryFlowReplayInput,
|
||||
} from '@klo/context/ingest/memory-flow';
|
||||
import { resolveKloConfigReference } from '@klo/context/core';
|
||||
import { loadKloProject } from '@klo/context/project';
|
||||
} from '@ktx/context/ingest/memory-flow';
|
||||
import { resolveKtxConfigReference } from '@ktx/context/core';
|
||||
import { loadKtxProject } from '@ktx/context/project';
|
||||
import {
|
||||
DEMO_ADAPTER,
|
||||
DEMO_CONNECTION_ID,
|
||||
|
|
@ -34,11 +34,11 @@ import {
|
|||
resolveFullCredentialDecision,
|
||||
type DemoPromptAdapter,
|
||||
} from './demo-interaction.js';
|
||||
import type { KloDoctorArgs } from './doctor.js';
|
||||
import type { KtxDoctorArgs } from './doctor.js';
|
||||
import {
|
||||
renderMemoryFlowTui,
|
||||
startLiveMemoryFlowTui,
|
||||
type KloMemoryFlowTuiIo,
|
||||
type KtxMemoryFlowTuiIo,
|
||||
type MemoryFlowTuiLiveSession,
|
||||
} from './memory-flow-tui.js';
|
||||
import {
|
||||
|
|
@ -51,36 +51,36 @@ import { formatNextStepLines } from './next-steps.js';
|
|||
|
||||
profileMark('module:demo');
|
||||
|
||||
export type KloDemoOutputMode = 'plain' | 'json' | 'viz';
|
||||
export type KloDemoInputMode = 'auto' | 'disabled';
|
||||
export type KloDemoMode = 'full' | 'seeded';
|
||||
export type KtxDemoOutputMode = 'plain' | 'json' | 'viz';
|
||||
export type KtxDemoInputMode = 'auto' | 'disabled';
|
||||
export type KtxDemoMode = 'full' | 'seeded';
|
||||
|
||||
export type KloDemoArgs =
|
||||
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
|
||||
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KloDemoInputMode }
|
||||
| { command: 'replay'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
|
||||
| { command: 'scan'; projectDir: string; inputMode?: KloDemoInputMode }
|
||||
| { command: 'inspect'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
|
||||
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KloDemoOutputMode, 'viz'>; inputMode?: KloDemoInputMode }
|
||||
| { command: 'seeded'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
|
||||
| { command: 'full'; projectDir: string; outputMode: KloDemoOutputMode; inputMode?: KloDemoInputMode }
|
||||
export type KtxDemoArgs =
|
||||
| { command: 'init'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'reset'; projectDir: string; force: boolean; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'replay'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'scan'; projectDir: string; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'inspect'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'doctor'; projectDir: string; outputMode: Exclude<KtxDemoOutputMode, 'viz'>; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'seeded'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
|
||||
| { command: 'full'; projectDir: string; outputMode: KtxDemoOutputMode; inputMode?: KtxDemoInputMode }
|
||||
| {
|
||||
command: 'ingest';
|
||||
mode: KloDemoMode;
|
||||
mode: KtxDemoMode;
|
||||
projectDir: string;
|
||||
outputMode: KloDemoOutputMode;
|
||||
inputMode?: KloDemoInputMode;
|
||||
outputMode: KtxDemoOutputMode;
|
||||
inputMode?: KtxDemoInputMode;
|
||||
};
|
||||
|
||||
export interface KloDemoIo {
|
||||
stdin?: KloMemoryFlowTuiIo['stdin'];
|
||||
export interface KtxDemoIo {
|
||||
stdin?: KtxMemoryFlowTuiIo['stdin'];
|
||||
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
}
|
||||
|
||||
interface KloDemoDeps {
|
||||
interface KtxDemoDeps {
|
||||
runFullDemo?: typeof runDemoFull;
|
||||
runDoctor?: (args: KloDoctorArgs, io: KloDemoIo) => Promise<number>;
|
||||
runDoctor?: (args: KtxDoctorArgs, io: KtxDemoIo) => Promise<number>;
|
||||
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
|
||||
startLiveMemoryFlow?: typeof startLiveMemoryFlowTui;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
|
@ -127,7 +127,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
|
|||
}
|
||||
}
|
||||
|
||||
const lines: string[] = ['', '★ KLO finished ingesting your data', ''];
|
||||
const lines: string[] = ['', '★ KTX finished ingesting your data', ''];
|
||||
|
||||
if (chunkCount > 0) {
|
||||
lines.push(` ✓ Analyzed ${chunkCount} business area${chunkCount === 1 ? '' : 's'}`);
|
||||
|
|
@ -137,7 +137,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
|
|||
lines.push('');
|
||||
|
||||
if (slCount > 0 || wikiCount > 0) {
|
||||
lines.push(' KLO created:');
|
||||
lines.push(' KTX created:');
|
||||
if (slCount > 0) lines.push(` 📊 ${slCount} query definition${slCount === 1 ? '' : 's'} — so agents can write accurate SQL for your data`);
|
||||
if (wikiCount > 0) lines.push(` 📝 ${wikiCount} knowledge page${wikiCount === 1 ? '' : 's'} — so agents understand your business context`);
|
||||
lines.push('');
|
||||
|
|
@ -153,7 +153,7 @@ function formatReplaySummary(input: MemoryFlowReplayInput): string {
|
|||
lines.push(...formatNextStepLines());
|
||||
if (input.sourceDir) {
|
||||
lines.push('');
|
||||
lines.push(` Your KLO project files are at: ${input.sourceDir}`);
|
||||
lines.push(` Your KTX project files are at: ${input.sourceDir}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ function formatPlainReplaySummary(input: MemoryFlowReplayInput): string {
|
|||
return [formatMemoryFlowFinalSummary(input).trimEnd(), '', 'What to do next:', ...formatNextStepLines(), ''].join('\n');
|
||||
}
|
||||
|
||||
function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode, io: KloDemoIo): void {
|
||||
function writeReplay(input: MemoryFlowReplayInput, outputMode: KtxDemoOutputMode, io: KtxDemoIo): void {
|
||||
if (outputMode === 'json') {
|
||||
io.stdout.write(`${JSON.stringify(input, null, 2)}\n`);
|
||||
return;
|
||||
|
|
@ -181,10 +181,10 @@ function writeReplay(input: MemoryFlowReplayInput, outputMode: KloDemoOutputMode
|
|||
|
||||
async function writeStoredReplay(
|
||||
input: MemoryFlowReplayInput,
|
||||
outputMode: KloDemoOutputMode,
|
||||
inputMode: KloDemoArgs['inputMode'],
|
||||
io: KloDemoIo,
|
||||
deps: KloDemoDeps,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
inputMode: KtxDemoArgs['inputMode'],
|
||||
io: KtxDemoIo,
|
||||
deps: KtxDemoDeps,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<void> {
|
||||
const resolvedOutputMode = effectiveDemoOutputMode(outputMode, io, env, {
|
||||
|
|
@ -211,8 +211,8 @@ async function writeStoredReplay(
|
|||
|
||||
function writeInspect(
|
||||
summary: Awaited<ReturnType<typeof inspectDemoProject>>,
|
||||
outputMode: KloDemoOutputMode,
|
||||
io: KloDemoIo,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
io: KtxDemoIo,
|
||||
): void {
|
||||
if (outputMode === 'json') {
|
||||
io.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
||||
|
|
@ -224,8 +224,8 @@ function writeInspect(
|
|||
|
||||
function writeFullDemo(
|
||||
result: Awaited<ReturnType<typeof runDemoFull>>,
|
||||
outputMode: KloDemoOutputMode,
|
||||
io: KloDemoIo,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
io: KtxDemoIo,
|
||||
options: { liveWasRendered?: boolean; projectDir?: string } = {},
|
||||
): void {
|
||||
if (outputMode === 'json') {
|
||||
|
|
@ -274,8 +274,8 @@ function replayWithFullMetadata(result: Awaited<ReturnType<typeof runDemoFull>>)
|
|||
|
||||
function pickMemoryFlowProgress(
|
||||
liveSession: MemoryFlowTuiLiveSession | null,
|
||||
outputMode: KloDemoOutputMode,
|
||||
io: KloDemoIo,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
io: KtxDemoIo,
|
||||
): ((snapshot: MemoryFlowReplayInput) => void) | undefined {
|
||||
if (liveSession) {
|
||||
return (snapshot: MemoryFlowReplayInput) => {
|
||||
|
|
@ -290,7 +290,7 @@ function pickMemoryFlowProgress(
|
|||
return createPlainProgressEmitter(io);
|
||||
}
|
||||
|
||||
function isTuiCapableDemoIo(io: KloDemoIo): io is KloDemoIo & KloMemoryFlowTuiIo {
|
||||
function isTuiCapableDemoIo(io: KtxDemoIo): io is KtxDemoIo & KtxMemoryFlowTuiIo {
|
||||
return (
|
||||
io.stdin?.isTTY === true &&
|
||||
io.stdout.isTTY === true &&
|
||||
|
|
@ -304,11 +304,11 @@ interface EffectiveDemoOutputModeOptions {
|
|||
}
|
||||
|
||||
function effectiveDemoOutputMode(
|
||||
outputMode: KloDemoOutputMode,
|
||||
io: KloDemoIo,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
io: KtxDemoIo,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: EffectiveDemoOutputModeOptions = {},
|
||||
): KloDemoOutputMode {
|
||||
): KtxDemoOutputMode {
|
||||
if (outputMode !== 'viz') {
|
||||
return outputMode;
|
||||
}
|
||||
|
|
@ -346,7 +346,7 @@ async function ensureDemoProjectForCommand(projectDir: string): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, deps: KloDemoDeps): Promise<string | null> {
|
||||
async function prepareProjectForDemoCommand(args: KtxDemoArgs, io: KtxDemoIo, deps: KtxDemoDeps): Promise<string | null> {
|
||||
if (args.command === 'init' || args.command === 'reset' || args.command === 'doctor') {
|
||||
return args.projectDir;
|
||||
}
|
||||
|
|
@ -372,10 +372,10 @@ async function prepareProjectForDemoCommand(args: KloDemoArgs, io: KloDemoIo, de
|
|||
|
||||
async function runReplayDemo(
|
||||
projectDir: string,
|
||||
outputMode: KloDemoOutputMode,
|
||||
inputMode: KloDemoArgs['inputMode'],
|
||||
io: KloDemoIo,
|
||||
deps: KloDemoDeps,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
inputMode: KtxDemoArgs['inputMode'],
|
||||
io: KtxDemoIo,
|
||||
deps: KtxDemoDeps,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<number> {
|
||||
await ensureDemoProjectForCommand(projectDir);
|
||||
|
|
@ -385,10 +385,10 @@ async function runReplayDemo(
|
|||
|
||||
async function runSeededDemo(
|
||||
projectDir: string,
|
||||
outputMode: KloDemoOutputMode,
|
||||
inputMode: KloDemoArgs['inputMode'],
|
||||
io: KloDemoIo,
|
||||
deps: KloDemoDeps,
|
||||
outputMode: KtxDemoOutputMode,
|
||||
inputMode: KtxDemoArgs['inputMode'],
|
||||
io: KtxDemoIo,
|
||||
deps: KtxDemoDeps,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<number> {
|
||||
const result = await runDemoSeeded({ projectDir });
|
||||
|
|
@ -411,7 +411,7 @@ async function runSeededDemo(
|
|||
return 0;
|
||||
}
|
||||
|
||||
export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, deps: KloDemoDeps = {}): Promise<number> {
|
||||
export async function runKtxDemo(args: KtxDemoArgs, io: KtxDemoIo = process, deps: KtxDemoDeps = {}): Promise<number> {
|
||||
try {
|
||||
if (args.command === 'init') {
|
||||
const result = await ensureDemoProject({ projectDir: args.projectDir, force: args.force });
|
||||
|
|
@ -419,7 +419,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
|
|||
io.stdout.write(`Config: ${result.configPath}\n`);
|
||||
io.stdout.write(`Database: ${result.databasePath}\n`);
|
||||
io.stdout.write(`Replay: ${result.replayPath}\n`);
|
||||
io.stdout.write('Next: klo setup demo --no-input\n');
|
||||
io.stdout.write('Next: ktx setup demo --no-input\n');
|
||||
io.stdout.write(' Runs the pre-seeded demo without calling the LLM.\n');
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -430,7 +430,7 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
|
|||
io.stdout.write(`Config: ${result.configPath}\n`);
|
||||
io.stdout.write(`Database: ${result.databasePath}\n`);
|
||||
io.stdout.write(`Replay: ${result.replayPath}\n`);
|
||||
io.stdout.write('Next: klo setup demo --mode full\n');
|
||||
io.stdout.write('Next: ktx setup demo --mode full\n');
|
||||
io.stdout.write(' Runs the full AI-backed pass with your LLM provider.\n');
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -454,13 +454,13 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
|
|||
if (args.command === 'full' || (args.command === 'ingest' && args.mode === 'full')) {
|
||||
const executeFullDemo = deps.runFullDemo ?? runDemoFull;
|
||||
await ensureDemoProjectForCommand(preparedProjectDir);
|
||||
const project = await loadKloProject({ projectDir: preparedProjectDir });
|
||||
const project = await loadKtxProject({ projectDir: preparedProjectDir });
|
||||
const credentialStatus = fullDemoCredentialStatus(project, env);
|
||||
const credentialDecision = await resolveFullCredentialDecision({
|
||||
needsAnthropicKey:
|
||||
credentialStatus.status === 'missing-anthropic-key' &&
|
||||
project.config.llm.provider.backend === 'anthropic' &&
|
||||
!resolveKloConfigReference(project.config.llm.provider.anthropic?.api_key, env),
|
||||
!resolveKtxConfigReference(project.config.llm.provider.anthropic?.api_key, env),
|
||||
inputMode: args.inputMode,
|
||||
io,
|
||||
env,
|
||||
|
|
@ -523,8 +523,8 @@ export async function runKloDemo(args: KloDemoArgs, io: KloDemoIo = process, dep
|
|||
}
|
||||
|
||||
if (args.command === 'doctor') {
|
||||
const { runKloDoctor } = await import('./doctor.js');
|
||||
const executeDoctor = deps.runDoctor ?? runKloDoctor;
|
||||
const { runKtxDoctor } = await import('./doctor.js');
|
||||
const executeDoctor = deps.runDoctor ?? runKtxDoctor;
|
||||
return await executeDoctor(
|
||||
{
|
||||
command: 'demo',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { runKloCli } from './index.js';
|
||||
import { runKtxCli } from './index.js';
|
||||
|
||||
function makeIo() {
|
||||
let stdout = '';
|
||||
|
|
@ -26,9 +26,9 @@ describe('dev Commander tree', () => {
|
|||
it('prints visible dev help with only supported low-level command groups', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
|
||||
for (const command of ['init', 'doctor', 'scan', 'ingest', 'mapping']) {
|
||||
expect(testIo.stdout()).toContain(command);
|
||||
}
|
||||
|
|
@ -51,10 +51,10 @@ describe('dev Commander tree', () => {
|
|||
it('keeps dev callable while hiding it from root command rows', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Advanced:');
|
||||
expect(testIo.stdout()).toContain('klo dev');
|
||||
expect(testIo.stdout()).toContain('ktx dev');
|
||||
expect(testIo.stdout()).not.toContain('dev Low-level diagnostics');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -63,15 +63,15 @@ describe('dev Commander tree', () => {
|
|||
const { mkdtemp, readFile, rm } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'klo-dev-init-'));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-'));
|
||||
const projectDir = join(tempDir, 'warehouse');
|
||||
const testIo = makeIo();
|
||||
|
||||
try {
|
||||
await expect(runKloCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
|
||||
await expect(readFile(join(projectDir, 'klo.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
|
|
@ -82,16 +82,16 @@ describe('dev Commander tree', () => {
|
|||
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(), 'klo-dev-init-global-'));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-dev-init-global-'));
|
||||
const projectDir = join(tempDir, 'global-init');
|
||||
const testIo = makeIo();
|
||||
|
||||
try {
|
||||
await expect(
|
||||
runKloCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
|
||||
runKtxCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KLO project at ${projectDir}`);
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
|
|
@ -106,7 +106,7 @@ describe('dev Commander tree', () => {
|
|||
]) {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
}
|
||||
|
|
@ -115,12 +115,12 @@ describe('dev Commander tree', () => {
|
|||
it.each([
|
||||
{
|
||||
argv: ['dev', 'doctor', '--help'],
|
||||
expected: ['Usage: klo dev doctor', '--json', '--no-input'],
|
||||
expected: ['Usage: ktx dev doctor', '--json', '--no-input'],
|
||||
},
|
||||
{
|
||||
argv: ['dev', 'scan', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan',
|
||||
'Usage: ktx dev scan',
|
||||
'--mode <mode>',
|
||||
'structural',
|
||||
'relationships',
|
||||
|
|
@ -136,12 +136,12 @@ describe('dev Commander tree', () => {
|
|||
},
|
||||
{
|
||||
argv: ['dev', 'scan', 'report', '--help'],
|
||||
expected: ['Usage: klo dev scan report [options] <runId>', '<runId>', '--json'],
|
||||
expected: ['Usage: ktx dev scan report [options] <runId>', '<runId>', '--json'],
|
||||
},
|
||||
{
|
||||
argv: ['dev', 'scan', 'relationships', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan relationships [options] <runId>',
|
||||
'Usage: ktx dev scan relationships [options] <runId>',
|
||||
'--status <status>',
|
||||
'--limit <count>',
|
||||
'--accept <candidateId>',
|
||||
|
|
@ -154,7 +154,7 @@ describe('dev Commander tree', () => {
|
|||
{
|
||||
argv: ['dev', 'scan', 'relationship-apply', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan relationship-apply [options] <runId>',
|
||||
'Usage: ktx dev scan relationship-apply [options] <runId>',
|
||||
'--all-accepted',
|
||||
'--candidate <candidateId>',
|
||||
'--dry-run',
|
||||
|
|
@ -163,7 +163,7 @@ describe('dev Commander tree', () => {
|
|||
{
|
||||
argv: ['dev', 'scan', 'relationship-thresholds', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan relationship-thresholds [options]',
|
||||
'Usage: ktx dev scan relationship-thresholds [options]',
|
||||
'--connection <connectionId>',
|
||||
'--min-total-labels <count>',
|
||||
'--min-accepted-labels <count>',
|
||||
|
|
@ -174,7 +174,7 @@ describe('dev Commander tree', () => {
|
|||
{
|
||||
argv: ['dev', 'scan', 'relationship-feedback', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan relationship-feedback [options]',
|
||||
'Usage: ktx dev scan relationship-feedback [options]',
|
||||
'--connection <connectionId>',
|
||||
'--decision <decision>',
|
||||
'--json',
|
||||
|
|
@ -184,7 +184,7 @@ describe('dev Commander tree', () => {
|
|||
{
|
||||
argv: ['dev', 'scan', 'relationship-calibration', '--help'],
|
||||
expected: [
|
||||
'Usage: klo dev scan relationship-calibration [options]',
|
||||
'Usage: ktx dev scan relationship-calibration [options]',
|
||||
'--connection <connectionId>',
|
||||
'--decision <decision>',
|
||||
'--accept-threshold <value>',
|
||||
|
|
@ -194,11 +194,11 @@ describe('dev Commander tree', () => {
|
|||
},
|
||||
{
|
||||
argv: ['dev', 'ingest', 'run', '--help'],
|
||||
expected: ['Usage: klo dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
|
||||
expected: ['Usage: ktx dev ingest run [options]', '--connection-id <connectionId>', '--adapter <adapter>'],
|
||||
},
|
||||
{
|
||||
argv: ['dev', 'mapping', 'sync-state', 'set', '--help'],
|
||||
expected: ['Usage: klo dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
|
||||
expected: ['Usage: ktx dev mapping sync-state set [options] <connectionId>', '--mode <mode>'],
|
||||
},
|
||||
])('prints generated nested help for $argv', async ({ argv, expected }) => {
|
||||
const io = makeIo();
|
||||
|
|
@ -206,7 +206,7 @@ describe('dev Commander tree', () => {
|
|||
const ingest = vi.fn(async () => 0);
|
||||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
|
||||
await expect(runKtxCli(argv, io.io, { doctor, ingest, scan })).resolves.toBe(0);
|
||||
|
||||
for (const text of expected) {
|
||||
expect(io.stdout()).toContain(text);
|
||||
|
|
@ -222,7 +222,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
|
||||
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--dry-run'], scanIo.io, { scan }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(scan).toHaveBeenCalledWith(
|
||||
|
|
@ -245,7 +245,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
|
||||
runKtxCli(['dev', 'scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'], io.io, {
|
||||
scan,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -269,7 +269,7 @@ describe('dev Commander tree', () => {
|
|||
const io = makeIo();
|
||||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
|
||||
await expect(runKtxCli(['dev', 'scan', 'warehouse', option], io.io, { scan })).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toContain(`unknown option '${option}'`);
|
||||
|
|
@ -279,18 +279,18 @@ describe('dev Commander tree', () => {
|
|||
const io = makeIo();
|
||||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
|
||||
await expect(runKtxCli(['dev', 'scan', '--dry-run'], io.io, { scan })).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('Usage: klo dev scan');
|
||||
expect(io.stderr()).toContain('klo dev scan requires <connectionId> or a subcommand');
|
||||
expect(io.stdout()).toContain('Usage: ktx dev scan');
|
||||
expect(io.stderr()).toContain('ktx dev scan requires <connectionId> or a subcommand');
|
||||
});
|
||||
|
||||
it('rejects invalid scan modes before dispatch', async () => {
|
||||
const io = makeIo();
|
||||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
|
||||
await expect(runKtxCli(['dev', 'scan', 'warehouse', '--mode', 'deep'], io.io, { scan })).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toContain("argument 'deep' is invalid");
|
||||
|
|
@ -301,10 +301,10 @@ describe('dev Commander tree', () => {
|
|||
const io = makeIo();
|
||||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], io.io, { scan })).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('--project-dir is inherited from `klo dev scan`');
|
||||
expect(io.stdout()).not.toContain('--project-dir is inherited from `klo scan`');
|
||||
expect(io.stdout()).toContain('--project-dir is inherited from `ktx dev scan`');
|
||||
expect(io.stdout()).not.toContain('--project-dir is inherited from `ktx scan`');
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -314,10 +314,10 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
|
||||
runKtxCli(['dev', 'scan', 'report', 'scan-run-1', '--project-dir', '/tmp/project'], humanIo.io, { scan }),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
|
||||
runKtxCli(['dev', 'scan', 'report', 'scan-run-2', '--project-dir', '/tmp/project', '--json'], jsonIo.io, {
|
||||
scan,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -339,7 +339,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -377,7 +377,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -419,7 +419,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
|
||||
runKtxCli(['dev', 'scan', 'relationships', 'scan-run-review', option, ''], io.io, { scan }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
|
|
@ -431,7 +431,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
|
||||
runKtxCli(['dev', 'scan', 'relationship-feedback', '--json', '--jsonl'], io.io, { scan }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
|
|
@ -443,7 +443,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -480,7 +480,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -516,7 +516,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -557,7 +557,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -598,7 +598,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
|
||||
runKtxCli(['dev', 'scan', 'relationship-calibration', '--accept-threshold', '1.5'], io.io, { scan }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
|
|
@ -610,7 +610,7 @@ describe('dev Commander tree', () => {
|
|||
const scan = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'scan',
|
||||
|
|
@ -635,7 +635,7 @@ describe('dev Commander tree', () => {
|
|||
const ingest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'ingest',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { resolve } from 'node:path';
|
||||
import type { Command } from '@commander-js/extra-typings';
|
||||
import { type CommandWithGlobalOptions, type KloCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
|
||||
import { type CommandWithGlobalOptions, type KtxCliCommandContext, resolveCommandProjectDir } from './cli-program.js';
|
||||
import { registerCompletionCommands } from './commands/completion-commands.js';
|
||||
import { registerConnectionMappingCommands } from './commands/connection-commands.js';
|
||||
import { registerDoctorCommands } from './commands/doctor-commands.js';
|
||||
|
|
@ -10,7 +10,7 @@ import { profileMark } from './startup-profile.js';
|
|||
|
||||
profileMark('module:dev');
|
||||
|
||||
export function registerDevCommands(program: Command, context: KloCliCommandContext): void {
|
||||
export function registerDevCommands(program: Command, context: KtxCliCommandContext): void {
|
||||
const dev = program
|
||||
.command('dev', { hidden: true })
|
||||
.description('Low-level diagnostics, scans, adapter commands, and mapping tools')
|
||||
|
|
@ -27,10 +27,10 @@ export function registerDevCommands(program: Command, context: KloCliCommandCont
|
|||
|
||||
dev
|
||||
.command('init')
|
||||
.description('Initialize a Git-backed KLO project directory for maintenance scripts')
|
||||
.description('Initialize a Git-backed KTX project directory for maintenance scripts')
|
||||
.argument('[directory]', 'Project directory')
|
||||
.option('--name <name>', 'Project name written to klo.yaml')
|
||||
.option('--force', 'Rewrite klo.yaml and scaffold files in an existing project', false)
|
||||
.option('--name <name>', 'Project name written to ktx.yaml')
|
||||
.option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
|
||||
.action(
|
||||
async (
|
||||
projectDir: string | undefined,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
|
||||
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
|
||||
import {
|
||||
formatDoctorReport,
|
||||
runKloDoctor,
|
||||
runKtxDoctor,
|
||||
runSetupDoctorChecks,
|
||||
type DoctorCheck,
|
||||
} from './doctor.js';
|
||||
|
|
@ -32,13 +32,13 @@ function makeIo() {
|
|||
}
|
||||
|
||||
type EmbeddingHealthCheck = (
|
||||
config: KloEmbeddingConfig,
|
||||
options?: KloEmbeddingHealthCheckOptions,
|
||||
) => Promise<KloEmbeddingHealthCheckResult>;
|
||||
config: KtxEmbeddingConfig,
|
||||
options?: KtxEmbeddingHealthCheckOptions,
|
||||
) => Promise<KtxEmbeddingHealthCheckResult>;
|
||||
|
||||
async function writeProjectConfig(projectDir: string, embeddingLines: string[]): Promise<void> {
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -69,9 +69,9 @@ describe('formatDoctorReport', () => {
|
|||
},
|
||||
];
|
||||
|
||||
expect(formatDoctorReport({ title: 'KLO setup doctor', checks })).toBe(
|
||||
expect(formatDoctorReport({ title: 'KTX setup doctor', checks })).toBe(
|
||||
[
|
||||
'KLO setup doctor',
|
||||
'KTX setup doctor',
|
||||
'PASS Node 22+: v22.16.0 ABI 127',
|
||||
'FAIL Native SQLite: Cannot load better-sqlite3',
|
||||
' Fix: Run: pnpm run native:rebuild',
|
||||
|
|
@ -85,12 +85,12 @@ describe('runSetupDoctorChecks', () => {
|
|||
it('returns pass checks when injected commands and file checks succeed', async () => {
|
||||
const checks = await runSetupDoctorChecks({
|
||||
env: { PATH: '/bin' },
|
||||
workspaceRoot: '/workspace/klo',
|
||||
workspaceRoot: '/workspace/ktx',
|
||||
execText: async (command, args) => {
|
||||
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
|
||||
if (command === 'corepack' && args[0] === '--version') return '0.32.0';
|
||||
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
|
||||
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
|
||||
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
|
||||
throw new Error(`${command} ${args.join(' ')}`);
|
||||
},
|
||||
pathExists: async () => true,
|
||||
|
|
@ -111,7 +111,7 @@ describe('runSetupDoctorChecks', () => {
|
|||
it('returns exact fixes when setup checks fail', async () => {
|
||||
const checks = await runSetupDoctorChecks({
|
||||
env: {},
|
||||
workspaceRoot: '/workspace/klo',
|
||||
workspaceRoot: '/workspace/ktx',
|
||||
execText: async (command) => {
|
||||
throw new Error(`${command} not found`);
|
||||
},
|
||||
|
|
@ -140,12 +140,12 @@ describe('runSetupDoctorChecks', () => {
|
|||
it('treats missing corepack as a warning so setup doctor can still pass', async () => {
|
||||
const checks = await runSetupDoctorChecks({
|
||||
env: { PATH: '/bin' },
|
||||
workspaceRoot: '/workspace/klo',
|
||||
workspaceRoot: '/workspace/ktx',
|
||||
execText: async (command, args) => {
|
||||
if (command === 'pnpm' && args[0] === '--version') return '10.28.0';
|
||||
if (command === 'corepack' && args[0] === '--version') throw new Error('spawn corepack ENOENT');
|
||||
if (command === 'uv' && args[0] === '--version') return 'uv 0.9.5';
|
||||
if (command === process.execPath && args.includes('--version')) return '@klo/cli 0.0.0-private';
|
||||
if (command === process.execPath && args.includes('--version')) return '@ktx/cli 0.0.0-private';
|
||||
throw new Error(`${command} ${args.join(' ')}`);
|
||||
},
|
||||
pathExists: async () => true,
|
||||
|
|
@ -154,7 +154,7 @@ describe('runSetupDoctorChecks', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
|
||||
runKtxDoctor({ command: 'setup', outputMode: 'plain', inputMode: 'disabled' }, testIo.io, {
|
||||
runSetupChecks: async () => checks,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -171,11 +171,11 @@ describe('runSetupDoctorChecks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('runKloDoctor', () => {
|
||||
describe('runKtxDoctor', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-doctor-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-doctor-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -186,7 +186,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'setup', outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -204,7 +204,7 @@ describe('runKloDoctor', () => {
|
|||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('KLO setup doctor');
|
||||
expect(testIo.stdout()).toContain('KTX setup doctor');
|
||||
expect(testIo.stdout()).toContain('FAIL TypeScript package build: Missing packages/cli/dist/bin.js');
|
||||
expect(testIo.stdout()).toContain('Fix: Run: pnpm run build');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
|
|
@ -214,7 +214,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'setup', outputMode: 'json', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -226,14 +226,14 @@ describe('runKloDoctor', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(JSON.parse(testIo.stdout())).toEqual({
|
||||
title: 'KLO setup doctor',
|
||||
title: 'KTX setup doctor',
|
||||
checks: [{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('runs project checks against a valid klo.yaml', async () => {
|
||||
it('runs project checks against a valid ktx.yaml', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -250,7 +250,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -261,14 +261,14 @@ describe('runKloDoctor', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('KLO project doctor');
|
||||
expect(testIo.stdout()).toContain('KTX project doctor');
|
||||
expect(testIo.stdout()).toContain('PASS Project config: warehouse');
|
||||
expect(testIo.stdout()).toContain('PASS Connections: 1 configured');
|
||||
});
|
||||
|
||||
it('includes Postgres historic-SQL readiness in project doctor output', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -295,12 +295,12 @@ describe('runKloDoctor', () => {
|
|||
status: 'warn' as const,
|
||||
detail:
|
||||
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
|
||||
fix: `Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${tempDir}\``,
|
||||
fix: `Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${tempDir}\``,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -322,7 +322,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -338,7 +338,7 @@ describe('runKloDoctor', () => {
|
|||
'Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
|
||||
);
|
||||
expect(testIo.stdout()).toContain(
|
||||
`Fix: Run: klo setup --project-dir ${tempDir} --no-input`,
|
||||
`Fix: Run: ktx setup --project-dir ${tempDir} --no-input`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -375,7 +375,7 @@ describe('runKloDoctor', () => {
|
|||
dimensions: 384,
|
||||
sentenceTransformers: { baseURL: 'http://127.0.0.1:8765', pathPrefix: '' },
|
||||
},
|
||||
{ text: 'KLO semantic search doctor probe', timeoutMs: 1234 },
|
||||
{ text: 'KTX semantic search doctor probe', timeoutMs: 1234 },
|
||||
);
|
||||
expect(testIo.stdout()).toContain(
|
||||
'PASS Semantic search embeddings: sentence-transformers/all-MiniLM-L6-v2 (384d) probe succeeded',
|
||||
|
|
@ -395,7 +395,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'plain', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -413,7 +413,7 @@ describe('runKloDoctor', () => {
|
|||
model: 'all-MiniLM-L6-v2',
|
||||
dimensions: 384,
|
||||
}),
|
||||
{ text: 'KLO semantic search doctor probe', timeoutMs: 120_000 },
|
||||
{ text: 'KTX semantic search doctor probe', timeoutMs: 120_000 },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ describe('runKloDoctor', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloDoctor(
|
||||
runKtxDoctor(
|
||||
{ command: 'project', projectDir: tempDir, outputMode: 'json', inputMode: 'disabled' },
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -454,7 +454,7 @@ describe('runKloDoctor', () => {
|
|||
status: 'warn',
|
||||
detail:
|
||||
'sentence-transformers/all-MiniLM-L6-v2 (384d) probe failed: connect ECONNREFUSED 127.0.0.1:8765. Semantic lane will be skipped; lexical, dictionary, and token lanes remain available.',
|
||||
fix: `Run: klo setup --project-dir ${tempDir} --no-input`,
|
||||
fix: `Run: ktx setup --project-dir ${tempDir} --no-input`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import { access } from 'node:fs/promises';
|
|||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import type { KloLocalProject, KloProjectEmbeddingConfig } from '@klo/context/project';
|
||||
import type { KloEmbeddingConfig, KloEmbeddingHealthCheckOptions, KloEmbeddingHealthCheckResult } from '@klo/llm';
|
||||
import type { KtxLocalProject, KtxProjectEmbeddingConfig } from '@ktx/context/project';
|
||||
import type { KtxEmbeddingConfig, KtxEmbeddingHealthCheckOptions, KtxEmbeddingHealthCheckResult } from '@ktx/llm';
|
||||
import type { HistoricSqlDoctorDeps } from './historic-sql-doctor.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
type DoctorStatus = 'pass' | 'warn' | 'fail';
|
||||
type KloDoctorOutputMode = 'plain' | 'json';
|
||||
type KloDoctorInputMode = 'auto' | 'disabled';
|
||||
type KtxDoctorOutputMode = 'plain' | 'json';
|
||||
type KtxDoctorInputMode = 'auto' | 'disabled';
|
||||
|
||||
export interface DoctorCheck {
|
||||
id: string;
|
||||
|
|
@ -27,12 +27,12 @@ interface DoctorReport {
|
|||
checks: DoctorCheck[];
|
||||
}
|
||||
|
||||
export type KloDoctorArgs =
|
||||
| { command: 'setup'; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
|
||||
| { command: 'project'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode }
|
||||
| { command: 'demo'; projectDir: string; outputMode: KloDoctorOutputMode; inputMode?: KloDoctorInputMode };
|
||||
export type KtxDoctorArgs =
|
||||
| { command: 'setup'; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
|
||||
| { command: 'project'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode }
|
||||
| { command: 'demo'; projectDir: string; outputMode: KtxDoctorOutputMode; inputMode?: KtxDoctorInputMode };
|
||||
|
||||
interface KloDoctorIo {
|
||||
interface KtxDoctorIo {
|
||||
stdout: { write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
}
|
||||
|
|
@ -46,9 +46,9 @@ interface SetupDoctorDeps {
|
|||
}
|
||||
|
||||
type EmbeddingHealthCheck = (
|
||||
config: KloEmbeddingConfig,
|
||||
options?: KloEmbeddingHealthCheckOptions,
|
||||
) => Promise<KloEmbeddingHealthCheckResult>;
|
||||
config: KtxEmbeddingConfig,
|
||||
options?: KtxEmbeddingHealthCheckOptions,
|
||||
) => Promise<KtxEmbeddingHealthCheckResult>;
|
||||
|
||||
interface SemanticSearchDoctorDeps {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
|
@ -56,9 +56,9 @@ interface SemanticSearchDoctorDeps {
|
|||
embeddingProbeTimeoutMs?: number;
|
||||
}
|
||||
|
||||
interface KloDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
|
||||
interface KtxDoctorDeps extends SemanticSearchDoctorDeps, HistoricSqlDoctorDeps {
|
||||
runSetupChecks?: () => Promise<DoctorCheck[]>;
|
||||
runHistoricSqlDoctorChecks?: (project: KloLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
|
||||
runHistoricSqlDoctorChecks?: (project: KtxLocalProject, deps: HistoricSqlDoctorDeps) => Promise<DoctorCheck[]>;
|
||||
}
|
||||
|
||||
function workspaceRootDir(): string {
|
||||
|
|
@ -119,18 +119,18 @@ function check(status: DoctorStatus, id: string, label: string, detail: string,
|
|||
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
|
||||
}
|
||||
|
||||
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KLO semantic search doctor probe';
|
||||
const SEMANTIC_SEARCH_HEALTH_TEXT = 'KTX semantic search doctor probe';
|
||||
const SEMANTIC_SEARCH_HEALTH_TIMEOUT_MS = 5_000;
|
||||
const SEMANTIC_SEARCH_LOCAL_HEALTH_TIMEOUT_MS = 120_000;
|
||||
|
||||
function semanticEmbeddingSetupFix(projectDir: string, backend: KloProjectEmbeddingConfig['backend']): string {
|
||||
function semanticEmbeddingSetupFix(projectDir: string, backend: KtxProjectEmbeddingConfig['backend']): string {
|
||||
if (backend === 'openai') {
|
||||
return `Set OPENAI_API_KEY or rerun: klo setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
|
||||
return `Set OPENAI_API_KEY or rerun: ktx setup --project-dir ${projectDir} --embedding-backend openai --no-input`;
|
||||
}
|
||||
return `Run: klo setup --project-dir ${projectDir} --no-input`;
|
||||
return `Run: ktx setup --project-dir ${projectDir} --no-input`;
|
||||
}
|
||||
|
||||
function embeddingConfigLabel(config: KloProjectEmbeddingConfig | KloEmbeddingConfig): string {
|
||||
function embeddingConfigLabel(config: KtxProjectEmbeddingConfig | KtxEmbeddingConfig): string {
|
||||
const model = config.model?.trim() || 'model not configured';
|
||||
return `${config.backend}/${model} (${config.dimensions}d)`;
|
||||
}
|
||||
|
|
@ -140,15 +140,15 @@ function semanticLaneFallbackDetail(reason: string): string {
|
|||
}
|
||||
|
||||
async function defaultEmbeddingHealthCheck(
|
||||
config: KloEmbeddingConfig,
|
||||
options?: KloEmbeddingHealthCheckOptions,
|
||||
): Promise<KloEmbeddingHealthCheckResult> {
|
||||
const { runKloEmbeddingHealthCheck } = await import('@klo/llm');
|
||||
return runKloEmbeddingHealthCheck(config, options);
|
||||
config: KtxEmbeddingConfig,
|
||||
options?: KtxEmbeddingHealthCheckOptions,
|
||||
): Promise<KtxEmbeddingHealthCheckResult> {
|
||||
const { runKtxEmbeddingHealthCheck } = await import('@ktx/llm');
|
||||
return runKtxEmbeddingHealthCheck(config, options);
|
||||
}
|
||||
|
||||
async function runSemanticSearchEmbeddingCheck(
|
||||
config: KloProjectEmbeddingConfig,
|
||||
config: KtxProjectEmbeddingConfig,
|
||||
projectDir: string,
|
||||
deps: SemanticSearchDoctorDeps = {},
|
||||
): Promise<DoctorCheck> {
|
||||
|
|
@ -163,8 +163,8 @@ async function runSemanticSearchEmbeddingCheck(
|
|||
}
|
||||
|
||||
try {
|
||||
const { resolveLocalKloEmbeddingConfig } = await import('@klo/context');
|
||||
const resolved = resolveLocalKloEmbeddingConfig(config, deps.env ?? process.env);
|
||||
const { resolveLocalKtxEmbeddingConfig } = await import('@ktx/context');
|
||||
const resolved = resolveLocalKtxEmbeddingConfig(config, deps.env ?? process.env);
|
||||
if (!resolved) {
|
||||
return check(
|
||||
'warn',
|
||||
|
|
@ -300,7 +300,7 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
|
|||
'workspace-cli',
|
||||
'Workspace-local CLI',
|
||||
failureMessage(error),
|
||||
'Run: pnpm run build && pnpm run klo -- --version',
|
||||
'Run: pnpm run build && pnpm run ktx -- --version',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -308,11 +308,11 @@ export async function runSetupDoctorChecks(deps: SetupDoctorDeps = {}): Promise<
|
|||
return checks;
|
||||
}
|
||||
|
||||
async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
|
||||
const { loadKloProject } = await import('@klo/context/project');
|
||||
async function runProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
|
||||
const { loadKtxProject } = await import('@ktx/context/project');
|
||||
const checks: DoctorCheck[] = [];
|
||||
try {
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
checks.push(check('pass', 'project-config', 'Project config', project.config.project));
|
||||
const connectionCount = Object.keys(project.config.connections).length;
|
||||
checks.push(
|
||||
|
|
@ -323,7 +323,7 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
|
|||
'connections',
|
||||
'Connections',
|
||||
'0 configured',
|
||||
'Add a connection to klo.yaml or run `klo setup demo init`',
|
||||
'Add a connection to ktx.yaml or run `ktx setup demo init`',
|
||||
),
|
||||
);
|
||||
checks.push(check('pass', 'storage', 'Storage', `${project.config.storage.state}/${project.config.storage.search}`));
|
||||
|
|
@ -339,20 +339,20 @@ async function runProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): P
|
|||
'project-config',
|
||||
'Project config',
|
||||
failureMessage(error),
|
||||
`Run: klo init ${projectDir} --name <project-name>`,
|
||||
`Run: ktx init ${projectDir} --name <project-name>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}): Promise<DoctorCheck[]> {
|
||||
async function runDemoProjectChecks(projectDir: string, deps: KtxDoctorDeps = {}): Promise<DoctorCheck[]> {
|
||||
const env = deps.env ?? process.env;
|
||||
const { DEMO_CONNECTION_ID, DEMO_REPLAY_FILE } = await import('./demo-assets.js');
|
||||
const { loadKloProject } = await import('@klo/context/project');
|
||||
const { loadKtxProject } = await import('@ktx/context/project');
|
||||
const checks: DoctorCheck[] = [];
|
||||
const requiredPaths = [
|
||||
['demo-config', 'Demo config', 'klo.yaml'],
|
||||
['demo-config', 'Demo config', 'ktx.yaml'],
|
||||
['demo-database', 'Demo dataset', 'demo.db'],
|
||||
['demo-state', 'Demo state database', 'state.sqlite'],
|
||||
['demo-replay', 'Demo replay', join('replays', DEMO_REPLAY_FILE)],
|
||||
|
|
@ -371,13 +371,13 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
|
|||
id,
|
||||
label,
|
||||
`Missing ${relativePath}`,
|
||||
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const connection = project.config.connections[DEMO_CONNECTION_ID];
|
||||
checks.push(
|
||||
connection?.driver === 'sqlite'
|
||||
|
|
@ -387,7 +387,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
|
|||
'demo-connection',
|
||||
'Demo connection',
|
||||
`${DEMO_CONNECTION_ID} is missing or is not sqlite`,
|
||||
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
),
|
||||
);
|
||||
const provider = project.config.llm.provider.backend;
|
||||
|
|
@ -399,7 +399,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
|
|||
'demo-llm-provider',
|
||||
'Demo LLM provider',
|
||||
provider,
|
||||
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
),
|
||||
);
|
||||
if (provider === 'anthropic' && !env.ANTHROPIC_API_KEY) {
|
||||
|
|
@ -409,7 +409,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
|
|||
'anthropic-credentials',
|
||||
'Anthropic credentials',
|
||||
'ANTHROPIC_API_KEY is not set',
|
||||
'Export ANTHROPIC_API_KEY to run `klo setup demo --mode full --no-input`',
|
||||
'Export ANTHROPIC_API_KEY to run `ktx setup demo --mode full --no-input`',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -426,7 +426,7 @@ async function runDemoProjectChecks(projectDir: string, deps: KloDoctorDeps = {}
|
|||
'demo-config-parse',
|
||||
'Demo config parse',
|
||||
failureMessage(error),
|
||||
`Run: klo setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
`Run: ktx setup demo init --project-dir ${projectDir} --force --no-input`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -450,7 +450,7 @@ function hasFailures(report: DoctorReport): boolean {
|
|||
return report.checks.some((item) => item.status === 'fail');
|
||||
}
|
||||
|
||||
function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io: KloDoctorIo): void {
|
||||
function writeReport(report: DoctorReport, outputMode: KtxDoctorOutputMode, io: KtxDoctorIo): void {
|
||||
if (outputMode === 'json') {
|
||||
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return;
|
||||
|
|
@ -458,24 +458,24 @@ function writeReport(report: DoctorReport, outputMode: KloDoctorOutputMode, io:
|
|||
io.stdout.write(formatDoctorReport(report));
|
||||
}
|
||||
|
||||
export async function runKloDoctor(
|
||||
args: KloDoctorArgs,
|
||||
io: KloDoctorIo = process,
|
||||
deps: KloDoctorDeps = {},
|
||||
export async function runKtxDoctor(
|
||||
args: KtxDoctorArgs,
|
||||
io: KtxDoctorIo = process,
|
||||
deps: KtxDoctorDeps = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
const runSetupChecks = deps.runSetupChecks ?? (() => runSetupDoctorChecks());
|
||||
const setupChecks = await runSetupChecks();
|
||||
const report: DoctorReport =
|
||||
args.command === 'setup'
|
||||
? { title: 'KLO setup doctor', checks: setupChecks }
|
||||
? { title: 'KTX setup doctor', checks: setupChecks }
|
||||
: args.command === 'demo'
|
||||
? {
|
||||
title: 'KLO demo doctor',
|
||||
title: 'KTX demo doctor',
|
||||
checks: [...setupChecks, ...(await runDemoProjectChecks(args.projectDir, deps))],
|
||||
}
|
||||
: {
|
||||
title: 'KLO project doctor',
|
||||
title: 'KTX project doctor',
|
||||
checks: [...setupChecks, ...(await runProjectChecks(args.projectDir, deps))],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe('standalone local warehouse example', () => {
|
|||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-example-smoke-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-example-smoke-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -128,14 +128,14 @@ describe('standalone local warehouse example', () => {
|
|||
]);
|
||||
expect(ingest).toMatchObject({ code: 1, stdout: '' });
|
||||
expect(ingest.stderr).toContain(
|
||||
'klo dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
|
||||
'ktx dev ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner',
|
||||
);
|
||||
}, 30_000);
|
||||
|
||||
it('serves local wiki and semantic-layer MCP tools against the copied example project', async () => {
|
||||
const projectDir = await copyExampleProject(tempDir);
|
||||
|
||||
const client = new Client({ name: 'klo-example-client', version: '0.0.0' });
|
||||
const client = new Client({ name: 'ktx-example-client', version: '0.0.0' });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [CLI_BIN, 'serve', '--mcp', 'stdio', '--project-dir', projectDir, '--user-id', 'example-user'],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { buildDefaultKloProjectConfig, type KloProjectConnectionConfig } from '@klo/context/project';
|
||||
import { HistoricSqlExtensionMissingError } from '@klo/context/ingest';
|
||||
import { buildDefaultKtxProjectConfig, type KtxProjectConnectionConfig } from '@ktx/context/project';
|
||||
import { HistoricSqlExtensionMissingError } from '@ktx/context/ingest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
runPostgresHistoricSqlDoctorChecks,
|
||||
|
|
@ -7,14 +7,14 @@ import {
|
|||
type PostgresHistoricSqlDoctorProbe,
|
||||
} from './historic-sql-doctor.js';
|
||||
|
||||
function projectWithConnections(connections: Record<string, KloProjectConnectionConfig>): HistoricSqlDoctorProject {
|
||||
function projectWithConnections(connections: Record<string, KtxProjectConnectionConfig>): HistoricSqlDoctorProject {
|
||||
return {
|
||||
projectDir: '/tmp/klo-project',
|
||||
projectDir: '/tmp/ktx-project',
|
||||
config: {
|
||||
...buildDefaultKloProjectConfig('warehouse'),
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
connections,
|
||||
ingest: {
|
||||
...buildDefaultKloProjectConfig('warehouse').ingest,
|
||||
...buildDefaultKtxProjectConfig('warehouse').ingest,
|
||||
adapters: ['live-database', 'historic-sql'],
|
||||
},
|
||||
},
|
||||
|
|
@ -61,7 +61,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
|
|||
);
|
||||
|
||||
expect(probe).toHaveBeenCalledWith({
|
||||
projectDir: '/tmp/klo-project',
|
||||
projectDir: '/tmp/ktx-project',
|
||||
connectionId: 'warehouse',
|
||||
connection: {
|
||||
driver: 'postgres',
|
||||
|
|
@ -108,7 +108,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
|
|||
status: 'warn',
|
||||
detail:
|
||||
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
|
||||
fix: 'Update the Postgres parameter group or config, then rerun `klo dev doctor --project-dir /tmp/klo-project`',
|
||||
fix: 'Update the Postgres parameter group or config, then rerun `ktx dev doctor --project-dir /tmp/ktx-project`',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import type { KloProjectConfig, KloProjectConnectionConfig } from '@klo/context/project';
|
||||
import type { KtxProjectConfig, KtxProjectConnectionConfig } from '@ktx/context/project';
|
||||
import type { DoctorCheck } from './doctor.js';
|
||||
|
||||
export interface HistoricSqlDoctorProject {
|
||||
projectDir: string;
|
||||
config: Pick<KloProjectConfig, 'connections' | 'ingest'>;
|
||||
config: Pick<KtxProjectConfig, 'connections' | 'ingest'>;
|
||||
}
|
||||
|
||||
export interface PostgresHistoricSqlDoctorProbeInput {
|
||||
projectDir: string;
|
||||
connectionId: string;
|
||||
connection: KloProjectConnectionConfig;
|
||||
connection: KtxProjectConnectionConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
|
|
@ -31,19 +31,19 @@ function check(status: DoctorCheck['status'], id: string, label: string, detail:
|
|||
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
|
||||
}
|
||||
|
||||
function historicSqlRecord(connection: KloProjectConnectionConfig): Record<string, unknown> | null {
|
||||
function historicSqlRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
|
||||
const historicSql = connection.historicSql;
|
||||
return historicSql && typeof historicSql === 'object' && !Array.isArray(historicSql)
|
||||
? (historicSql as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function isEnabledPostgresHistoricSql(connection: KloProjectConnectionConfig): boolean {
|
||||
function isEnabledPostgresHistoricSql(connection: KtxProjectConnectionConfig): boolean {
|
||||
const historicSql = historicSqlRecord(connection);
|
||||
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
|
||||
}
|
||||
|
||||
function isPostgresDriver(connection: KloProjectConnectionConfig): boolean {
|
||||
function isPostgresDriver(connection: KtxProjectConnectionConfig): boolean {
|
||||
const driver = String(connection.driver ?? '').toLowerCase();
|
||||
return driver === 'postgres' || driver === 'postgresql';
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ function capabilityFailureFix(error: unknown, connectionId: string, projectDir:
|
|||
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
|
||||
return 'Use PostgreSQL 14 or newer, or disable historicSql for this connection';
|
||||
}
|
||||
return `Fix connections.${connectionId} Postgres settings, then rerun \`klo dev doctor --project-dir ${projectDir}\``;
|
||||
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx dev doctor --project-dir ${projectDir}\``;
|
||||
}
|
||||
|
||||
function failureDetail(error: unknown): string {
|
||||
|
|
@ -75,14 +75,14 @@ function failureDetail(error: unknown): string {
|
|||
async function defaultPostgresHistoricSqlProbe(
|
||||
input: PostgresHistoricSqlDoctorProbeInput,
|
||||
): Promise<PostgresHistoricSqlDoctorProbeResult> {
|
||||
const [{ PostgresPgssQueryHistoryReader }, { KloPostgresHistoricSqlQueryClient, isKloPostgresConnectionConfig }] =
|
||||
await Promise.all([import('@klo/context/ingest'), import('@klo/connector-postgres')]);
|
||||
const [{ PostgresPgssQueryHistoryReader }, { KtxPostgresHistoricSqlQueryClient, isKtxPostgresConnectionConfig }] =
|
||||
await Promise.all([import('@ktx/context/ingest'), import('@ktx/connector-postgres')]);
|
||||
|
||||
if (!isKloPostgresConnectionConfig(input.connection)) {
|
||||
if (!isKtxPostgresConnectionConfig(input.connection)) {
|
||||
throw new Error(`Native PostgreSQL connector cannot run driver "${input.connection.driver ?? 'unknown'}"`);
|
||||
}
|
||||
|
||||
const client = new KloPostgresHistoricSqlQueryClient({
|
||||
const client = new KtxPostgresHistoricSqlQueryClient({
|
||||
connectionId: input.connectionId,
|
||||
connection: input.connection,
|
||||
env: input.env,
|
||||
|
|
@ -135,7 +135,7 @@ export async function runPostgresHistoricSqlDoctorChecks(
|
|||
checkId(connectionId),
|
||||
label,
|
||||
`pg_stat_statements ready (${result.pgServerVersion}) with warnings: ${result.warnings.join('; ')}`,
|
||||
`Update the Postgres parameter group or config, then rerun \`klo dev doctor --project-dir ${project.projectDir}\``,
|
||||
`Update the Postgres parameter group or config, then rerun \`ktx dev doctor --project-dir ${project.projectDir}\``,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { join } from 'node:path';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getKloCliPackageInfo,
|
||||
getKtxCliPackageInfo,
|
||||
rendererUnavailableVizFallback,
|
||||
renderMemoryFlowTui,
|
||||
resolveVizFallback,
|
||||
runKloCli,
|
||||
runKtxCli,
|
||||
sanitizeMemoryFlowTuiError,
|
||||
startLiveMemoryFlowTui,
|
||||
warnVizFallbackOnce,
|
||||
|
|
@ -39,20 +39,20 @@ function makeIo(options: { stdoutIsTty?: boolean } = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
describe('getKloCliPackageInfo', () => {
|
||||
describe('getKtxCliPackageInfo', () => {
|
||||
it('identifies the CLI package and its context dependency', () => {
|
||||
expect(getKloCliPackageInfo()).toEqual({
|
||||
name: '@klo/cli',
|
||||
expect(getKtxCliPackageInfo()).toEqual({
|
||||
name: '@ktx/cli',
|
||||
version: '0.0.0-private',
|
||||
contextPackageName: '@klo/context',
|
||||
contextPackageName: '@ktx/context',
|
||||
});
|
||||
});
|
||||
|
||||
it('exports package metadata for package managers and runtime diagnostics', () => {
|
||||
const packageJson = require('@klo/cli/package.json') as { name: string; version: string };
|
||||
const packageJson = require('@ktx/cli/package.json') as { name: string; version: string };
|
||||
|
||||
expect(packageJson).toMatchObject({
|
||||
name: '@klo/cli',
|
||||
name: '@ktx/cli',
|
||||
version: '0.0.0-private',
|
||||
});
|
||||
});
|
||||
|
|
@ -82,11 +82,11 @@ describe('memory-flow renderer exports', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('runKloCli', () => {
|
||||
describe('runKtxCli', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -96,18 +96,18 @@ describe('runKloCli', () => {
|
|||
it('prints version information', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['--version'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toBe('@klo/cli 0.0.0-private\n');
|
||||
expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('prints the May 6 public command surface in root help', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'serve', 'status']) {
|
||||
expect(testIo.stdout()).toContain(`${command}`);
|
||||
}
|
||||
|
|
@ -116,22 +116,22 @@ describe('runKloCli', () => {
|
|||
expect(testIo.stdout()).not.toContain(`${removed} `);
|
||||
}
|
||||
expect(testIo.stdout()).toContain('--project-dir <path>');
|
||||
expect(testIo.stdout()).toContain('KLO_PROJECT_DIR');
|
||||
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('klo dev');
|
||||
expect(testIo.stdout()).toContain('ktx dev');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('exposes demo under setup help instead of root help', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo setup [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx setup [options] [command]');
|
||||
expect(testIo.stdout()).toContain('demo');
|
||||
expect(testIo.stdout()).toContain('Run the packaged KLO demo from setup');
|
||||
expect(testIo.stdout()).toContain('Run the packaged KTX demo from setup');
|
||||
expect(testIo.stdout()).not.toContain('--skip-llm');
|
||||
expect(testIo.stdout()).not.toContain('--skip-embeddings');
|
||||
expect(testIo.stdout()).not.toContain('--embedding-model');
|
||||
|
|
@ -140,33 +140,33 @@ describe('runKloCli', () => {
|
|||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('prints help for bare klo outside a TTY', async () => {
|
||||
it('prints help for bare ktx outside a TTY', async () => {
|
||||
const setup = vi.fn(async () => 0);
|
||||
const testIo = makeIo({ stdoutIsTty: false });
|
||||
|
||||
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('starts setup for bare klo in a TTY when no project is discoverable', async () => {
|
||||
it('starts setup for bare ktx in a TTY when no project is discoverable', async () => {
|
||||
const { mkdtemp, realpath, rm } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const originalCwd = process.cwd();
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-setup-'));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-setup-'));
|
||||
const setup = vi.fn(async () => 0);
|
||||
const testIo = makeIo({ stdoutIsTty: true });
|
||||
const previousProjectDir = process.env.KLO_PROJECT_DIR;
|
||||
const previousProjectDir = process.env.KTX_PROJECT_DIR;
|
||||
const expectedProjectDir = await realpath(tempDir);
|
||||
|
||||
try {
|
||||
delete process.env.KLO_PROJECT_DIR;
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
process.chdir(tempDir);
|
||||
|
||||
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
|
||||
expect(setup).toHaveBeenCalledWith(
|
||||
{
|
||||
|
|
@ -187,71 +187,71 @@ describe('runKloCli', () => {
|
|||
},
|
||||
testIo.io,
|
||||
);
|
||||
expect(testIo.stdout()).not.toContain('Usage: klo [options] [command]');
|
||||
expect(testIo.stdout()).not.toContain('Usage: ktx [options] [command]');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
if (previousProjectDir === undefined) {
|
||||
delete process.env.KLO_PROJECT_DIR;
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
} else {
|
||||
process.env.KLO_PROJECT_DIR = previousProjectDir;
|
||||
process.env.KTX_PROJECT_DIR = previousProjectDir;
|
||||
}
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('prints help without project status for bare klo in a TTY when a project is discoverable', async () => {
|
||||
it('prints help without project status for bare ktx in a TTY when a project is discoverable', async () => {
|
||||
const { mkdtemp, realpath, rm, writeFile } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
const originalCwd = process.cwd();
|
||||
const previousProjectDir = process.env.KLO_PROJECT_DIR;
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'klo-bare-existing-'));
|
||||
const previousProjectDir = process.env.KTX_PROJECT_DIR;
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-bare-existing-'));
|
||||
const setup = vi.fn(async () => 0);
|
||||
const testIo = makeIo({ stdoutIsTty: true });
|
||||
const expectedProjectDir = await realpath(tempDir);
|
||||
|
||||
try {
|
||||
delete process.env.KLO_PROJECT_DIR;
|
||||
await writeFile(join(tempDir, 'klo.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
process.chdir(tempDir);
|
||||
|
||||
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
expect(testIo.stdout()).not.toContain(`Project: ${expectedProjectDir}`);
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
if (previousProjectDir === undefined) {
|
||||
delete process.env.KLO_PROJECT_DIR;
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
} else {
|
||||
process.env.KLO_PROJECT_DIR = previousProjectDir;
|
||||
process.env.KTX_PROJECT_DIR = previousProjectDir;
|
||||
}
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not invoke status for bare klo in a TTY when status would fail', async () => {
|
||||
it('does not invoke status for bare ktx in a TTY when status would fail', async () => {
|
||||
const setup = vi.fn(async () => {
|
||||
throw new Error('Unsupported ingest.llm: use top-level llm.provider, llm.models, and ingest.workUnits');
|
||||
});
|
||||
const testIo = makeIo({ stdoutIsTty: true });
|
||||
const previousProjectDir = process.env.KLO_PROJECT_DIR;
|
||||
const previousProjectDir = process.env.KTX_PROJECT_DIR;
|
||||
|
||||
try {
|
||||
process.env.KLO_PROJECT_DIR = tempDir;
|
||||
process.env.KTX_PROJECT_DIR = tempDir;
|
||||
|
||||
await expect(runKloCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
if (previousProjectDir === undefined) {
|
||||
delete process.env.KLO_PROJECT_DIR;
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
} else {
|
||||
process.env.KLO_PROJECT_DIR = previousProjectDir;
|
||||
process.env.KTX_PROJECT_DIR = previousProjectDir;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -260,7 +260,7 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const removedVerboseOption = '--' + 'verbose';
|
||||
|
||||
await expect(runKloCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli([removedVerboseOption, 'connection', 'list'], testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toContain(`unknown option '${removedVerboseOption}'`);
|
||||
expect(testIo.stdout()).toBe('');
|
||||
|
|
@ -270,12 +270,12 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const zshWords = '$' + '{words[@]}';
|
||||
|
||||
await expect(runKloCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'completion', 'zsh'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('#compdef klo');
|
||||
expect(testIo.stdout()).toContain('KLO_COMPLETION_COMMAND:-klo');
|
||||
expect(testIo.stdout()).toContain('#compdef ktx');
|
||||
expect(testIo.stdout()).toContain('KTX_COMPLETION_COMMAND:-ktx');
|
||||
expect(testIo.stdout()).toContain(`dev __complete --shell zsh --position "$CURRENT" -- "${zshWords}"`);
|
||||
expect(testIo.stdout()).toContain('compdef _klo klo');
|
||||
expect(testIo.stdout()).toContain('compdef _ktx ktx');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -283,22 +283,22 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const previousHome = process.env.HOME;
|
||||
const previousZdotdir = process.env.ZDOTDIR;
|
||||
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
|
||||
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
|
||||
|
||||
try {
|
||||
process.env.HOME = tempHome;
|
||||
delete process.env.ZDOTDIR;
|
||||
|
||||
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
const completionFile = await readFile(join(tempHome, '.zfunc', '_klo'), 'utf-8');
|
||||
const completionFile = await readFile(join(tempHome, '.zfunc', '_ktx'), 'utf-8');
|
||||
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
|
||||
expect(completionFile).toContain('#compdef klo');
|
||||
expect(zshrc).toContain('# >>> klo completion >>>');
|
||||
expect(zshrc).toContain('_klo_completion_command()');
|
||||
expect(zshrc).toContain('"name": "klo-workspace"');
|
||||
expect(zshrc).toContain('scripts/run-klo.mjs');
|
||||
expect(zshrc).toContain("export KLO_COMPLETION_COMMAND='$(_klo_completion_command)'");
|
||||
expect(completionFile).toContain('#compdef ktx');
|
||||
expect(zshrc).toContain('# >>> ktx completion >>>');
|
||||
expect(zshrc).toContain('_ktx_completion_command()');
|
||||
expect(zshrc).toContain('"name": "ktx-workspace"');
|
||||
expect(zshrc).toContain('scripts/run-ktx.mjs');
|
||||
expect(zshrc).toContain("export KTX_COMPLETION_COMMAND='$(_ktx_completion_command)'");
|
||||
expect(zshrc).toContain('setopt complete_aliases');
|
||||
expect(zshrc).toContain('fpath=("$HOME/.zfunc" $fpath)');
|
||||
expect(zshrc).toContain('autoload -Uz compinit');
|
||||
|
|
@ -326,20 +326,20 @@ describe('runKloCli', () => {
|
|||
const secondIo = makeIo();
|
||||
const previousHome = process.env.HOME;
|
||||
const previousZdotdir = process.env.ZDOTDIR;
|
||||
const tempHome = await mkdtemp(join(tmpdir(), 'klo-completion-home-'));
|
||||
const tempHome = await mkdtemp(join(tmpdir(), 'ktx-completion-home-'));
|
||||
|
||||
try {
|
||||
process.env.HOME = tempHome;
|
||||
delete process.env.ZDOTDIR;
|
||||
await writeFile(join(tempHome, '.zshrc'), 'export EDITOR=vim\nautoload -Uz compinit\ncompinit\n', 'utf-8');
|
||||
|
||||
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
|
||||
await expect(runKloCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], firstIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'completion', 'zsh', '--install'], secondIo.io)).resolves.toBe(0);
|
||||
|
||||
const zshrc = await readFile(join(tempHome, '.zshrc'), 'utf-8');
|
||||
expect(zshrc.match(/# >>> klo completion >>>/g)).toHaveLength(1);
|
||||
expect(zshrc.match(/# >>> ktx completion >>>/g)).toHaveLength(1);
|
||||
expect(zshrc.indexOf('fpath=("$HOME/.zfunc" $fpath)')).toBeLessThan(zshrc.indexOf('autoload -Uz compinit'));
|
||||
expect(zshrc.match(/_klo_completion_command\(\)/g)).toHaveLength(1);
|
||||
expect(zshrc.match(/_ktx_completion_command\(\)/g)).toHaveLength(1);
|
||||
expect(zshrc.match(/^compinit$/gm)).toHaveLength(1);
|
||||
expect(secondIo.stdout()).toContain('Updated zsh config:');
|
||||
expect(firstIo.stderr()).toBe('');
|
||||
|
|
@ -364,11 +364,11 @@ describe('runKloCli', () => {
|
|||
const connectionIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'klo', 'co'], rootIo.io),
|
||||
runKtxCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', 'co'], rootIo.io),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(
|
||||
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'klo', 'connection', 'm'],
|
||||
runKtxCli(
|
||||
['dev', '__complete', '--shell', 'zsh', '--position', '3', '--', 'ktx', 'connection', 'm'],
|
||||
connectionIo.io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -386,13 +386,13 @@ describe('runKloCli', () => {
|
|||
const choiceIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'klo', 'connection', 'add', '--cr'],
|
||||
runKtxCli(
|
||||
['dev', '__complete', '--shell', 'zsh', '--position', '4', '--', 'ktx', 'connection', 'add', '--cr'],
|
||||
optionIo.io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'__complete',
|
||||
|
|
@ -401,7 +401,7 @@ describe('runKloCli', () => {
|
|||
'--position',
|
||||
'7',
|
||||
'--',
|
||||
'klo',
|
||||
'ktx',
|
||||
'connection',
|
||||
'add',
|
||||
'notion',
|
||||
|
|
@ -425,7 +425,7 @@ describe('runKloCli', () => {
|
|||
const serveStdio = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'serve', '--mcp', 'stdio', '--user-id', 'agent'], testIo.io, {
|
||||
serveStdio,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -447,7 +447,7 @@ describe('runKloCli', () => {
|
|||
const ingest = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
|
||||
runKtxCli(['--project-dir', '/tmp/project', 'ingest', 'warehouse'], testIo.io, { publicIngest: ingest }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
|
|
@ -469,10 +469,10 @@ describe('runKloCli', () => {
|
|||
const lowLevelIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
|
||||
runKtxCli(['ingest', 'watch', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo ingest watch [options] [runId]');
|
||||
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');
|
||||
|
|
@ -488,12 +488,12 @@ describe('runKloCli', () => {
|
|||
const publicIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, {
|
||||
publicIngest,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -527,7 +527,7 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const demo = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKloCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
|
||||
await expect(runKtxCli(['demo', '--mode', 'replay', '--no-input'], testIo.io, { demo })).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/i);
|
||||
expect(demo).not.toHaveBeenCalled();
|
||||
|
|
@ -538,7 +538,7 @@ describe('runKloCli', () => {
|
|||
const demo = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'replay', '--no-input'], testIo.io, { demo }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(demo).toHaveBeenCalledWith(
|
||||
|
|
@ -553,7 +553,7 @@ describe('runKloCli', () => {
|
|||
|
||||
demo.mockClear();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', '--mode', 'seeded', '--no-input'], testIo.io, {
|
||||
demo,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -569,7 +569,7 @@ describe('runKloCli', () => {
|
|||
|
||||
demo.mockClear();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', '--mode', 'seeded'], testIo.io, {
|
||||
demo,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -585,7 +585,7 @@ describe('runKloCli', () => {
|
|||
|
||||
demo.mockClear();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'inspect', '--no-input'], testIo.io, { demo }),
|
||||
).resolves.toBe(0);
|
||||
expect(demo).toHaveBeenCalledWith(
|
||||
{
|
||||
|
|
@ -603,7 +603,7 @@ describe('runKloCli', () => {
|
|||
const demo = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input'], testIo.io, {
|
||||
demo,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -621,7 +621,7 @@ describe('runKloCli', () => {
|
|||
|
||||
demo.mockClear();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', '--no-input', 'demo', 'ingest', '--mode', 'seeded'], testIo.io, {
|
||||
demo,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -639,7 +639,7 @@ describe('runKloCli', () => {
|
|||
|
||||
demo.mockClear();
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'setup', 'demo', 'ingest', '--mode', 'full', '--no-input', '--plain'],
|
||||
testIo.io,
|
||||
{
|
||||
|
|
@ -665,16 +665,16 @@ describe('runKloCli', () => {
|
|||
const publicIngest = vi.fn();
|
||||
const lowLevelIngest = vi.fn();
|
||||
|
||||
await expect(runKloCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
|
||||
expect(testIo.stdout()).toContain('Build and refresh KLO context from configured sources');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
|
||||
expect(testIo.stdout()).toContain('Build and refresh KTX context from configured sources');
|
||||
expect(testIo.stdout()).toContain('status');
|
||||
expect(testIo.stdout()).toContain('watch');
|
||||
expect(testIo.stdout()).toContain('klo ingest --all [options]');
|
||||
expect(testIo.stdout()).toContain('klo ingest status [runId] [options]');
|
||||
expect(testIo.stdout()).toContain('klo ingest watch [runId] [options]');
|
||||
expect(testIo.stdout()).not.toContain('klo ingest replay <runId> [options]');
|
||||
expect(testIo.stdout()).toContain('ktx ingest --all [options]');
|
||||
expect(testIo.stdout()).toContain('ktx ingest status [runId] [options]');
|
||||
expect(testIo.stdout()).toContain('ktx ingest watch [runId] [options]');
|
||||
expect(testIo.stdout()).not.toContain('ktx ingest replay <runId> [options]');
|
||||
expect(testIo.stdout()).toContain('--no-input');
|
||||
expect(testIo.stdout()).not.toContain('--adapter');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
|
|
@ -689,20 +689,20 @@ describe('runKloCli', () => {
|
|||
const publicIngest = vi.fn(async () => 0);
|
||||
const lowLevelIngest = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
|
||||
await expect(runKtxCli(['ingest', 'run'], publicRunIo.io, { publicIngest, ingest: lowLevelIngest })).resolves.toBe(
|
||||
1,
|
||||
);
|
||||
expect(publicRunIo.stderr()).toMatch(/invalid argument|reserved|run/i);
|
||||
expect(publicIngest).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
runKloCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
|
||||
runKtxCli(['ingest', 'run', '--help'], publicHelpIo.io, { publicIngest, ingest: lowLevelIngest }),
|
||||
).resolves.toBe(0);
|
||||
expect(publicHelpIo.stdout()).toContain('Usage: klo ingest [options] [connectionId]');
|
||||
expect(publicHelpIo.stdout()).not.toContain('Usage: klo ingest ' + 'run');
|
||||
expect(publicHelpIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]');
|
||||
expect(publicHelpIo.stdout()).not.toContain('Usage: ktx ingest ' + 'run');
|
||||
|
||||
await expect(
|
||||
runKloCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
|
||||
runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
|
||||
publicIngest,
|
||||
ingest: lowLevelIngest,
|
||||
}),
|
||||
|
|
@ -720,11 +720,11 @@ describe('runKloCli', () => {
|
|||
const ingestRunIo = makeIo();
|
||||
const ingestReplayHelpIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
|
||||
await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(
|
||||
0,
|
||||
);
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'dev',
|
||||
'ingest',
|
||||
|
|
@ -746,7 +746,7 @@ describe('runKloCli', () => {
|
|||
{ ingest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(runKloCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0);
|
||||
|
||||
expect(doctor).toHaveBeenCalledWith({ command: 'setup', outputMode: 'json', inputMode: 'disabled' }, doctorIo.io);
|
||||
expect(ingest).toHaveBeenCalledWith(
|
||||
|
|
@ -763,7 +763,7 @@ describe('runKloCli', () => {
|
|||
},
|
||||
ingestRunIo.io,
|
||||
);
|
||||
expect(ingestReplayHelpIo.stdout()).toContain('Usage: klo dev ingest replay [options] <runId>');
|
||||
expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx dev ingest replay [options] <runId>');
|
||||
expect(ingestReplayHelpIo.stdout()).toContain('<runId>');
|
||||
expect(doctorIo.stderr()).toBe('');
|
||||
expect(ingestRunIo.stderr()).toBe('');
|
||||
|
|
@ -774,7 +774,7 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const connection = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
|
|
@ -788,9 +788,9 @@ describe('runKloCli', () => {
|
|||
const statusIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'status', '--json'], setupIo.io, { setup }),
|
||||
).resolves.toBe(0);
|
||||
await expect(runKloCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'status', '--json'], statusIo.io, { setup })).resolves.toBe(0);
|
||||
|
||||
expect(setup).toHaveBeenNthCalledWith(1, { command: 'status', projectDir: tempDir, json: true }, setupIo.io);
|
||||
expect(setup).toHaveBeenNthCalledWith(2, { command: 'status', projectDir: tempDir, json: true }, statusIo.io);
|
||||
|
|
@ -803,21 +803,21 @@ describe('runKloCli', () => {
|
|||
const statusIo = makeIo();
|
||||
const stopIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'build'], buildIo.io, { setup })).resolves.toBe(
|
||||
0,
|
||||
);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'watch', 'setup-context-local-1'], watchIo.io, {
|
||||
setup,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'status', 'setup-context-local-1', '--json'], statusIo.io, {
|
||||
setup,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'context', 'stop', 'setup-context-local-1'], stopIo.io, {
|
||||
setup,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -849,7 +849,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -883,7 +883,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -907,7 +907,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -943,7 +943,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'setup',
|
||||
'--project-dir',
|
||||
|
|
@ -997,7 +997,7 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -1045,7 +1045,7 @@ describe('runKloCli', () => {
|
|||
const removeIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -1064,7 +1064,7 @@ describe('runKloCli', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', 'remove', '--agents'], removeIo.io, { setup }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(setup).toHaveBeenNthCalledWith(
|
||||
|
|
@ -1088,7 +1088,7 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -1115,7 +1115,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'deterministic'], setupIo.io, { setup }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
|
|
@ -1127,7 +1127,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', '--embedding-backend', 'gateway'], setupIo.io, { setup }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(setup).not.toHaveBeenCalled();
|
||||
|
|
@ -1139,7 +1139,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -1165,7 +1165,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'setup', '--enable-historic-sql', '--disable-historic-sql'], setupIo.io, {
|
||||
setup,
|
||||
}),
|
||||
).resolves.toBe(1);
|
||||
|
|
@ -1179,12 +1179,12 @@ describe('runKloCli', () => {
|
|||
const toolsIo = makeIo();
|
||||
const agent = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['agent', '--help'], helpIo.io, { agent })).resolves.toBe(0);
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
|
||||
runKtxCli(['--project-dir', tempDir, 'agent', 'tools', '--json'], toolsIo.io, { agent }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(helpIo.stdout()).toContain('Usage: klo agent');
|
||||
expect(helpIo.stdout()).toContain('Usage: ktx agent');
|
||||
expect(toolsIo.stderr()).toBe('');
|
||||
expect(agent).toHaveBeenCalledWith({ command: 'tools', projectDir: tempDir, json: true }, toolsIo.io);
|
||||
});
|
||||
|
|
@ -1277,13 +1277,13 @@ describe('runKloCli', () => {
|
|||
|
||||
for (const entry of cases) {
|
||||
const io = makeIo();
|
||||
await expect(runKloCli(entry.argv, io.io, { agent })).resolves.toBe(0);
|
||||
await expect(runKtxCli(entry.argv, io.io, { agent })).resolves.toBe(0);
|
||||
expect(agent).toHaveBeenLastCalledWith(entry.args, io.io);
|
||||
expect(io.stderr()).toBe('');
|
||||
}
|
||||
|
||||
const helpIo = makeIo();
|
||||
await expect(runKloCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['--help'], helpIo.io, { agent })).resolves.toBe(0);
|
||||
expect(helpIo.stdout()).not.toContain('agent ');
|
||||
});
|
||||
|
||||
|
|
@ -1323,7 +1323,7 @@ describe('runKloCli', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'agent', 'sl', 'list', '--json', '--connection-id', 'warehouse', '--query', 'paid'],
|
||||
io.io,
|
||||
{ agent },
|
||||
|
|
@ -1376,7 +1376,7 @@ describe('runKloCli', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'agent', 'wiki', 'search', 'paid order', '--json', '--limit', '5'], io.io, {
|
||||
agent,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -1394,23 +1394,23 @@ describe('runKloCli', () => {
|
|||
});
|
||||
|
||||
it('dispatches public connection subcommands through the existing connection implementation', async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'klo-connection-dispatch-'));
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-connection-dispatch-'));
|
||||
const connection = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
|
||||
runKtxCli(['--project-dir', tempDir, 'connection', 'list'], makeIo().io, { connection }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const removeIo = makeIo();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'connection', 'remove', 'warehouse', '--force', '--no-input'], removeIo.io, {
|
||||
connection,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const mapIo = makeIo();
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
|
||||
runKtxCli(['--project-dir', tempDir, 'connection', 'map', 'prod-metabase', '--json'], mapIo.io, {
|
||||
connection,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -1444,9 +1444,9 @@ describe('runKloCli', () => {
|
|||
it('prints help for connection metabase setup', async () => {
|
||||
const helpIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['connection', 'metabase', 'setup', '--help'], helpIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(helpIo.stdout()).toContain('Usage: klo connection metabase setup');
|
||||
expect(helpIo.stdout()).toContain('Usage: ktx connection metabase setup');
|
||||
for (const option of [
|
||||
'--id <connectionId>',
|
||||
'--url <url>',
|
||||
|
|
@ -1465,10 +1465,10 @@ describe('runKloCli', () => {
|
|||
}
|
||||
expect(helpIo.stdout()).toContain('Guided equivalent of:');
|
||||
for (const line of [
|
||||
'klo connection mapping refresh <connectionId> --auto-accept',
|
||||
'klo connection mapping set <connectionId> databaseMappings <id>=<target>',
|
||||
'klo connection mapping set-sync-enabled <connectionId> <id> --enabled true',
|
||||
'klo ingest <connectionId>',
|
||||
'ktx connection mapping refresh <connectionId> --auto-accept',
|
||||
'ktx connection mapping set <connectionId> databaseMappings <id>=<target>',
|
||||
'ktx connection mapping set-sync-enabled <connectionId> <id> --enabled true',
|
||||
'ktx ingest <connectionId>',
|
||||
]) {
|
||||
expect(helpIo.stdout()).toContain(line);
|
||||
}
|
||||
|
|
@ -1481,7 +1481,7 @@ describe('runKloCli', () => {
|
|||
const setupIo = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'connection',
|
||||
'metabase',
|
||||
|
|
@ -1598,7 +1598,7 @@ describe('runKloCli', () => {
|
|||
],
|
||||
]) {
|
||||
const testIo = makeIo();
|
||||
await expect(runKloCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, testIo.io, { connectionMetabaseSetup })).resolves.toBe(1);
|
||||
expect(testIo.stderr()).toMatch(/map|sync|sync-mode|conflict|cannot be used|invalid|integer|choices/i);
|
||||
}
|
||||
|
||||
|
|
@ -1615,7 +1615,7 @@ describe('runKloCli', () => {
|
|||
]) {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(argv, testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
}
|
||||
|
|
@ -1626,7 +1626,7 @@ describe('runKloCli', () => {
|
|||
const connection = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'connection',
|
||||
'add',
|
||||
|
|
@ -1688,9 +1688,9 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const connectionNotion = vi.fn(async () => 0);
|
||||
|
||||
await expect(runKloCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
|
||||
await expect(runKtxCli(argv, testIo.io, { connectionNotion })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo connection notion');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx connection notion');
|
||||
expect(testIo.stdout()).toContain('pick');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(connectionNotion).not.toHaveBeenCalled();
|
||||
|
|
@ -1702,7 +1702,7 @@ describe('runKloCli', () => {
|
|||
const connectionNotion = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'--project-dir',
|
||||
tempDir,
|
||||
|
|
@ -1739,7 +1739,7 @@ describe('runKloCli', () => {
|
|||
const connectionNotion = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
|
||||
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--root-page-id', 'not-a-uuid'], testIo.io, {
|
||||
connectionNotion,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -1761,7 +1761,7 @@ describe('runKloCli', () => {
|
|||
const connectionNotion = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
|
||||
runKtxCli(['connection', 'notion', 'pick', 'notion-main', '--no-input'], testIo.io, { connectionNotion }),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(connectionNotion).not.toHaveBeenCalled();
|
||||
|
|
@ -1773,18 +1773,18 @@ describe('runKloCli', () => {
|
|||
const connection = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(
|
||||
runKloCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
|
||||
runKtxCli(['--project-dir', tempDir, '--debug', 'connection', 'list'], testIo.io, { connection }),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stderr()).toContain(`[debug] projectDir=${tempDir}`);
|
||||
expect(testIo.stderr()).toContain('[debug] dispatch=connection');
|
||||
});
|
||||
|
||||
it('routes low-level scan through klo dev with top-level project-dir', async () => {
|
||||
it('routes low-level scan through ktx dev with top-level project-dir', async () => {
|
||||
const testIo = makeIo();
|
||||
const scan = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKloCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse'], testIo.io, { scan })).resolves.toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
|
|
@ -1807,7 +1807,7 @@ describe('runKloCli', () => {
|
|||
const serveStdio = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'serve',
|
||||
'--mcp',
|
||||
|
|
@ -1843,9 +1843,9 @@ describe('runKloCli', () => {
|
|||
it('prints dev help for bare dev commands', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['dev'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo dev [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
|
||||
expect(testIo.stdout()).toContain('Low-level diagnostics');
|
||||
expect(testIo.stdout()).toContain('scan');
|
||||
expect(testIo.stdout()).toContain('ingest');
|
||||
|
|
@ -1857,15 +1857,15 @@ describe('runKloCli', () => {
|
|||
|
||||
it('prints dev command help without invoking low-level execution', async () => {
|
||||
for (const [command, expected] of [
|
||||
['scan', ['Usage: klo dev scan', '--dry-run', 'status', 'report']],
|
||||
['ingest', ['Usage: klo dev ingest', 'run', 'replay']],
|
||||
['mapping', ['Usage: klo dev mapping', 'sync-state', 'validate']],
|
||||
['scan', ['Usage: ktx dev scan', '--dry-run', 'status', 'report']],
|
||||
['ingest', ['Usage: ktx dev ingest', 'run', 'replay']],
|
||||
['mapping', ['Usage: ktx dev mapping', 'sync-state', 'validate']],
|
||||
] as const) {
|
||||
const testIo = makeIo();
|
||||
const scan = vi.fn().mockResolvedValue(0);
|
||||
const sl = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKloCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', command, '--help'], testIo.io, { scan, sl })).resolves.toBe(0);
|
||||
|
||||
for (const text of expected) {
|
||||
expect(testIo.stdout()).toContain(text);
|
||||
|
|
@ -1880,9 +1880,9 @@ describe('runKloCli', () => {
|
|||
const testIo = makeIo();
|
||||
const scan = vi.fn().mockResolvedValue(0);
|
||||
|
||||
await expect(runKloCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'scan', 'report', '--help'], testIo.io, { scan })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: klo dev scan report [options] <runId>');
|
||||
expect(testIo.stdout()).toContain('Usage: ktx dev scan report [options] <runId>');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(scan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -1890,7 +1890,7 @@ describe('runKloCli', () => {
|
|||
it('rejects removed reserved dev subcommands', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toMatch(/unknown command|error:/);
|
||||
});
|
||||
|
|
@ -1906,7 +1906,7 @@ describe('runKloCli', () => {
|
|||
['setup', 'demo', 'replay', '--json', '--plain'],
|
||||
]) {
|
||||
const testIo = makeIo();
|
||||
await expect(runKloCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
|
||||
await expect(runKtxCli(argv, testIo.io, { ingest, demo })).resolves.toBe(1);
|
||||
expect(testIo.stderr()).toMatch(/conflict|cannot be used/i);
|
||||
}
|
||||
|
||||
|
|
@ -1920,7 +1920,7 @@ describe('runKloCli', () => {
|
|||
|
||||
const tokenIo = makeIo();
|
||||
await expect(
|
||||
runKloCli(
|
||||
runKtxCli(
|
||||
[
|
||||
'connection',
|
||||
'add',
|
||||
|
|
@ -1946,14 +1946,14 @@ describe('runKloCli', () => {
|
|||
it('validates connection mapping set syntax before runner domain validation', async () => {
|
||||
const badFieldIo = makeIo();
|
||||
await expect(
|
||||
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
|
||||
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'invalidMappings', '1=warehouse'], badFieldIo.io),
|
||||
).resolves.toBe(1);
|
||||
expect(badFieldIo.stderr()).toContain('databaseMappings or connectionMappings');
|
||||
|
||||
for (const assignment of ['missing-equals', '=warehouse', '1=']) {
|
||||
const testIo = makeIo();
|
||||
await expect(
|
||||
runKloCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
|
||||
runKtxCli(['connection', 'mapping', 'set', 'prod-metabase', 'databaseMappings', assignment], testIo.io),
|
||||
).resolves.toBe(1);
|
||||
expect(testIo.stderr()).toContain('non-empty <key>=<value>');
|
||||
}
|
||||
|
|
@ -1962,7 +1962,7 @@ describe('runKloCli', () => {
|
|||
it('does not expose root init after setup owns project creation', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['init'], testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli(['init'], testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toContain("error: unknown command 'init'");
|
||||
});
|
||||
|
|
@ -1970,7 +1970,7 @@ describe('runKloCli', () => {
|
|||
it('returns an error code for unknown commands', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKloCli(['unknown'], testIo.io)).resolves.toBe(1);
|
||||
await expect(runKtxCli(['unknown'], testIo.io)).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stderr()).toContain("error: unknown command 'unknown'");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,48 +1,48 @@
|
|||
import { profileMark } from './startup-profile.js';
|
||||
|
||||
export {
|
||||
getKloCliPackageInfo,
|
||||
getKtxCliPackageInfo,
|
||||
runInitForCommander,
|
||||
runKloCli,
|
||||
type KloCliDeps,
|
||||
type KloCliIo,
|
||||
type KloCliPackageInfo,
|
||||
runKtxCli,
|
||||
type KtxCliDeps,
|
||||
type KtxCliIo,
|
||||
type KtxCliPackageInfo,
|
||||
} from './cli-runtime.js';
|
||||
export { runKloAgent, type KloAgentArgs } from './agent.js';
|
||||
export { runKtxAgent, type KtxAgentArgs } from './agent.js';
|
||||
export {
|
||||
KLO_AGENT_MAX_ROWS_CAP,
|
||||
createKloAgentRuntime,
|
||||
KTX_AGENT_MAX_ROWS_CAP,
|
||||
createKtxAgentRuntime,
|
||||
parseAgentMaxRows,
|
||||
readAgentJsonFile,
|
||||
writeAgentJson,
|
||||
writeAgentJsonError,
|
||||
type KloAgentRuntime,
|
||||
type KloAgentRuntimeDeps,
|
||||
type KtxAgentRuntime,
|
||||
type KtxAgentRuntimeDeps,
|
||||
} from './agent-runtime.js';
|
||||
export { runKloSetup, type KloSetupArgs, type KloSetupStatus } from './setup.js';
|
||||
export { runKtxSetup, type KtxSetupArgs, type KtxSetupStatus } from './setup.js';
|
||||
export type {
|
||||
KloSetupDatabaseDriver,
|
||||
KloSetupDatabasesArgs,
|
||||
KloSetupDatabasesDeps,
|
||||
KloSetupDatabasesResult,
|
||||
KtxSetupDatabaseDriver,
|
||||
KtxSetupDatabasesArgs,
|
||||
KtxSetupDatabasesDeps,
|
||||
KtxSetupDatabasesResult,
|
||||
} from './setup-databases.js';
|
||||
export { runKloSetupDatabasesStep } from './setup-databases.js';
|
||||
export { runKtxSetupDatabasesStep } from './setup-databases.js';
|
||||
export type {
|
||||
KloSetupEmbeddingBackend,
|
||||
KloSetupEmbeddingsArgs,
|
||||
KloSetupEmbeddingsDeps,
|
||||
KloSetupEmbeddingsResult,
|
||||
KtxSetupEmbeddingBackend,
|
||||
KtxSetupEmbeddingsArgs,
|
||||
KtxSetupEmbeddingsDeps,
|
||||
KtxSetupEmbeddingsResult,
|
||||
} from './setup-embeddings.js';
|
||||
export { runKloSetupEmbeddingsStep } from './setup-embeddings.js';
|
||||
export { runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
|
||||
export type {
|
||||
KloSetupSourcesArgs,
|
||||
KloSetupSourcesDeps,
|
||||
KloSetupSourcesPromptAdapter,
|
||||
KloSetupSourcesResult,
|
||||
KloSetupSourceType,
|
||||
KtxSetupSourcesArgs,
|
||||
KtxSetupSourcesDeps,
|
||||
KtxSetupSourcesPromptAdapter,
|
||||
KtxSetupSourcesResult,
|
||||
KtxSetupSourceType,
|
||||
} from './setup-sources.js';
|
||||
export { runKloSetupSourcesStep } from './setup-sources.js';
|
||||
export type { KloMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
|
||||
export { runKtxSetupSourcesStep } from './setup-sources.js';
|
||||
export type { KtxMemoryFlowTuiIo, MemoryFlowTuiLiveSession } from './memory-flow-tui.js';
|
||||
export {
|
||||
renderMemoryFlowTui,
|
||||
sanitizeMemoryFlowTuiError,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe('readIngestReportSnapshotFile', () => {
|
|||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-report-file-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-report-file-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@klo/context/ingest';
|
||||
import { parseIngestReportSnapshot, type IngestReportSnapshot } from '@ktx/context/ingest';
|
||||
|
||||
export async function readIngestReportSnapshotFile(reportFile: string): Promise<IngestReportSnapshot> {
|
||||
const raw = await readFile(reportFile, 'utf-8');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
|
|||
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { AgentRunnerService, type RunLoopParams } from '@klo/context/agent';
|
||||
import { AgentRunnerService, type RunLoopParams } from '@ktx/context/agent';
|
||||
import {
|
||||
LocalLookerRuntimeStore,
|
||||
LocalMetabaseSourceStateReader,
|
||||
|
|
@ -22,10 +22,10 @@ import {
|
|||
type RunLocalIngestOptions,
|
||||
type SourceAdapter,
|
||||
type SqliteBundleIngestStore,
|
||||
} from '@klo/context/ingest';
|
||||
import { initKloProject, kloLocalStateDbPath, loadKloProject } from '@klo/context/project';
|
||||
} from '@ktx/context/ingest';
|
||||
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type KloIngestArgs, runKloIngest } from './ingest.js';
|
||||
import { type KtxIngestArgs, runKtxIngest } from './ingest.js';
|
||||
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
|
||||
|
||||
function makeIo(
|
||||
|
|
@ -104,7 +104,7 @@ function makeIo(
|
|||
|
||||
async function writeWarehouseConfig(projectDir: string): Promise<void> {
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -123,7 +123,7 @@ async function writeWarehouseConfig(projectDir: string): Promise<void> {
|
|||
|
||||
async function writeMetabaseConfig(projectDir: string): Promise<void> {
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -438,9 +438,9 @@ type SyncModeCase = {
|
|||
|
||||
async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCase): Promise<void> {
|
||||
const projectDir = join(tempDir, `metabase-sync-mode-${input.name}`);
|
||||
await initKloProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
|
||||
await initKtxProject({ projectDir, projectName: `metabase-sync-mode-${input.name}` });
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
`project: metabase-sync-mode-${input.name}`,
|
||||
'connections:',
|
||||
|
|
@ -461,8 +461,8 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
|
|||
'utf-8',
|
||||
);
|
||||
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
|
||||
await store.replaceSourceState({
|
||||
connectionId: 'prod-metabase',
|
||||
syncMode: input.syncMode,
|
||||
|
|
@ -490,7 +490,7 @@ async function runPublicMetabaseSyncModeCase(tempDir: string, input: SyncModeCas
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -640,10 +640,10 @@ function localFakeBundleReport(jobId: string, overrides: Partial<IngestReportSna
|
|||
}
|
||||
|
||||
async function localBundleStore(projectDir: string, ids: [string, string]): Promise<SqliteBundleIngestStore> {
|
||||
const { SqliteBundleIngestStore } = await import('@klo/context/ingest');
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const { SqliteBundleIngestStore } = await import('@ktx/context/ingest');
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
return new SqliteBundleIngestStore({
|
||||
dbPath: kloLocalStateDbPath(project),
|
||||
dbPath: ktxLocalStateDbPath(project),
|
||||
idFactory: (() => {
|
||||
let index = 0;
|
||||
return () => ids[index++] ?? `generated-${index}`;
|
||||
|
|
@ -696,7 +696,7 @@ function emitLiveLocalMemoryFlow(memoryFlow: MemoryFlowEventSink | undefined): v
|
|||
memoryFlow?.finish('done');
|
||||
}
|
||||
|
||||
describe('runKloIngest', () => {
|
||||
describe('runKtxIngest', () => {
|
||||
let tempDir: string;
|
||||
let originalTerm: string | undefined;
|
||||
|
||||
|
|
@ -704,7 +704,7 @@ describe('runKloIngest', () => {
|
|||
resetVizFallbackWarningsForTest();
|
||||
originalTerm = process.env.TERM;
|
||||
process.env.TERM = 'xterm-256color';
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-ingest-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-ingest-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -718,7 +718,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('runs local ingest and reads status', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -731,7 +731,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const runIo = makeIo();
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -757,7 +757,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const statusIo = makeIo();
|
||||
await expect(
|
||||
runKloIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
|
||||
runKtxIngest({ command: 'status', projectDir, runId: 'cli-local-run-1', outputMode: 'plain' }, statusIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(statusIo.stdout()).toContain('Report: report-live-1');
|
||||
|
|
@ -770,7 +770,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('routes metabase scheduled pulls to the fan-out runner and prints child summaries', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeMetabaseConfig(projectDir);
|
||||
const io = makeIo();
|
||||
const report = localFakeBundleReport('metabase-child-1', {
|
||||
|
|
@ -782,7 +782,7 @@ describe('runKloIngest', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -828,7 +828,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('prints Metabase fan-out progress before the final summary', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeMetabaseConfig(projectDir);
|
||||
const io = makeIo();
|
||||
const report = localFakeBundleReport('metabase-child-1', {
|
||||
|
|
@ -840,7 +840,7 @@ describe('runKloIngest', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -908,9 +908,9 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('runs Metabase scheduled ingest through the public CLI command path with real fan-out', async () => {
|
||||
const projectDir = join(tempDir, 'metabase-cli-project');
|
||||
await initKloProject({ projectDir, projectName: 'metabase-cli' });
|
||||
await initKtxProject({ projectDir, projectName: 'metabase-cli' });
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: metabase-cli',
|
||||
'connections:',
|
||||
|
|
@ -933,12 +933,12 @@ describe('runKloIngest', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: kloLocalStateDbPath(project) });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const store = new LocalMetabaseSourceStateReader({ dbPath: ktxLocalStateDbPath(project) });
|
||||
await store.replaceSourceState({
|
||||
connectionId: 'prod-metabase',
|
||||
syncMode: 'ALL',
|
||||
defaultTagNames: ['klo'],
|
||||
defaultTagNames: ['ktx'],
|
||||
selections: [],
|
||||
mappings: [
|
||||
{
|
||||
|
|
@ -969,7 +969,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1001,7 +1001,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const statusIo = makeIo();
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{ command: 'status', projectDir, runId: 'metabase-child-1', outputMode: 'plain' },
|
||||
statusIo.io,
|
||||
),
|
||||
|
|
@ -1046,12 +1046,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('prints metabase fan-out JSON results', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeMetabaseConfig(projectDir);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1081,12 +1081,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('rejects source-dir uploads through the metabase fan-out route', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeMetabaseConfig(projectDir);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1105,13 +1105,13 @@ describe('runKloIngest', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain('source-dir uploads are not supported for the Metabase fan-out adapter');
|
||||
expect(io.stderr()).not.toContain('klo dev ingest run requires llm.provider.backend');
|
||||
expect(io.stderr()).not.toContain('ktx dev ingest run requires llm.provider.backend');
|
||||
expect(io.stdout()).toBe('');
|
||||
});
|
||||
|
||||
it('prints previous run and diff summary for local ingest results', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1120,7 +1120,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1145,15 +1145,15 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('passes the debug LLM request file to local ingest runs', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const runLocalIngest = vi.fn(async (input: RunLocalIngestOptions) =>
|
||||
completedLocalBundleRun(input, 'job-debug'),
|
||||
);
|
||||
const io = makeIo();
|
||||
const debugFile = join(projectDir, '.klo', 'llm-debug.jsonl');
|
||||
const debugFile = join(projectDir, '.ktx', 'llm-debug.jsonl');
|
||||
|
||||
const exitCode = await runKloIngest(
|
||||
const exitCode = await runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1172,7 +1172,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('passes daemon database introspection URL to default local ingest adapters', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1187,7 +1187,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1196,7 +1196,7 @@ describe('runKloIngest', () => {
|
|||
sourceDir,
|
||||
databaseIntrospectionUrl: 'http://127.0.0.1:8765',
|
||||
outputMode: 'plain',
|
||||
} satisfies KloIngestArgs,
|
||||
} satisfies KtxIngestArgs,
|
||||
io.io,
|
||||
{
|
||||
createAdapters,
|
||||
|
|
@ -1220,9 +1220,9 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('passes the target connection id when constructing local historic-sql adapters', async () => {
|
||||
const projectDir = join(tempDir, 'historic-sql-project');
|
||||
await initKloProject({ projectDir, projectName: 'historic-sql-project' });
|
||||
await initKtxProject({ projectDir, projectName: 'historic-sql-project' });
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: historic-sql-project',
|
||||
'connections:',
|
||||
|
|
@ -1250,7 +1250,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1281,7 +1281,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('passes local Looker pull-config options and agent runner into scheduled ingest for Looker scheduled ingest', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const pullConfigOptions = {
|
||||
looker: {
|
||||
|
|
@ -1299,14 +1299,14 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
connectionId: 'warehouse',
|
||||
adapter: 'fake',
|
||||
outputMode: 'plain',
|
||||
} satisfies KloIngestArgs,
|
||||
} satisfies KtxIngestArgs,
|
||||
io.io,
|
||||
{
|
||||
createAdapters,
|
||||
|
|
@ -1335,9 +1335,9 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('runs Looker scheduled ingest through the public CLI command path', async () => {
|
||||
const projectDir = join(tempDir, 'looker-project');
|
||||
await initKloProject({ projectDir, projectName: 'looker-cli' });
|
||||
await initKtxProject({ projectDir, projectName: 'looker-cli' });
|
||||
await writeFile(
|
||||
join(projectDir, 'klo.yaml'),
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: looker-cli',
|
||||
'connections:',
|
||||
|
|
@ -1357,8 +1357,8 @@ describe('runKloIngest', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir });
|
||||
const store = new LocalLookerRuntimeStore({ dbPath: kloLocalStateDbPath(project) });
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const store = new LocalLookerRuntimeStore({ dbPath: ktxLocalStateDbPath(project) });
|
||||
await store.setCursors('prod-looker', {
|
||||
dashboardsLastSyncedAt: null,
|
||||
looksLastSyncedAt: null,
|
||||
|
|
@ -1366,7 +1366,7 @@ describe('runKloIngest', () => {
|
|||
await store.upsertConnectionMapping({
|
||||
lookerConnectionId: 'prod-looker',
|
||||
lookerConnectionName: 'analytics',
|
||||
kloConnectionId: 'prod-warehouse',
|
||||
ktxConnectionId: 'prod-warehouse',
|
||||
source: 'cli',
|
||||
});
|
||||
const runtimeClient = makeCliLookerRuntimeClient();
|
||||
|
|
@ -1375,7 +1375,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1419,7 +1419,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const statusIo = makeIo();
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{ command: 'status', projectDir, runId: 'cli-looker-job', outputMode: 'plain' },
|
||||
statusIo.io,
|
||||
),
|
||||
|
|
@ -1431,7 +1431,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('renders live memory-flow frames for run --viz when stdout is interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1462,7 +1462,7 @@ describe('runKloIngest', () => {
|
|||
const startLiveMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => null);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1483,15 +1483,15 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.objectContaining({ memoryFlow: expect.any(Object) }));
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
expect((io.stdout().match(/KLO memory flow/g) ?? []).length).toBeGreaterThan(1);
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect((io.stdout().match(/KTX memory flow/g) ?? []).length).toBeGreaterThan(1);
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('fake-orders');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('uses the TUI live session for run --viz when stdin and stdout are interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1510,7 +1510,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1539,13 +1539,13 @@ describe('runKloIngest', () => {
|
|||
expect(liveSession.update).toHaveBeenCalled();
|
||||
expect(liveSession.close).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('prints a final plain summary after live viz completes', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
const liveSession = {
|
||||
|
|
@ -1560,7 +1560,7 @@ describe('runKloIngest', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1580,7 +1580,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('falls back to text live rendering when the TUI live session is unavailable', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1594,7 +1594,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1614,12 +1614,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(startLiveMemoryFlow).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('falls back to text live rendering when TUI startup fails with a redacted warning', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1638,7 +1638,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1657,13 +1657,13 @@ describe('runKloIngest', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stderr()).toContain('TUI visualization unavailable: Failed [redacted-url] [redacted]');
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
});
|
||||
|
||||
it('does not start live TUI when run --viz disables input', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1680,7 +1680,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1697,12 +1697,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('does not attach a live memory-flow sink for plain run output', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1712,7 +1712,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1728,12 +1728,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
|
||||
expect(io.stdout()).toContain('Job: plain-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
});
|
||||
|
||||
it('falls back to plain run output for run --viz when stdout is not interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1742,7 +1742,7 @@ describe('runKloIngest', () => {
|
|||
const io = makeIo({ isTTY: false });
|
||||
const runLocal = vi.fn(async (input: RunLocalIngestOptions) => completedLocalBundleRun(input, 'non-tty-viz-run'));
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1760,7 +1760,7 @@ describe('runKloIngest', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Job: non-tty-viz-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -1768,7 +1768,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('falls back to plain run output for run --viz when stdin raw mode is unavailable', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1783,7 +1783,7 @@ describe('runKloIngest', () => {
|
|||
}));
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir,
|
||||
|
|
@ -1804,7 +1804,7 @@ describe('runKloIngest', () => {
|
|||
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
|
||||
expect(io.stdout()).toContain('Job: raw-missing-viz-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -1812,11 +1812,11 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('returns an error code for missing status', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
|
||||
runKtxIngest({ command: 'status', projectDir, runId: 'missing-run', outputMode: 'plain' }, io.io),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain('Local ingest run or report "missing-run" was not found');
|
||||
|
|
@ -1824,13 +1824,13 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('uses the latest local ingest report when status has no run id', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
await persistLocalBundleReport(projectDir, localFakeBundleReport('older-run'));
|
||||
await persistLocalBundleReport(projectDir, localFakeBundleReport('newer-run'));
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKloIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
|
||||
await expect(runKtxIngest({ command: 'status', projectDir, outputMode: 'plain' }, io.io)).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Run: run-newer-run');
|
||||
expect(io.stdout()).toContain('Job: newer-run');
|
||||
|
|
@ -1839,28 +1839,28 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('renders the latest local ingest report through watch when run id is omitted', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
await persistLocalBundleReport(projectDir, localFakeBundleReport('watch-latest'));
|
||||
const io = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKloIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
|
||||
runKtxIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('Run: run-watch-latest');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('renders report-file replay through the memory-flow TUI', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const reportFile = await writeBundleReportFile(tempDir);
|
||||
const io = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -1873,7 +1873,7 @@ describe('runKloIngest', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/metabase done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/metabase done');
|
||||
expect(io.stdout()).toContain('Saved 2 memories from 2 raw files');
|
||||
expect(io.stdout()).toContain('Commit: abc12345 Run: run-1 Report: report-1');
|
||||
expect(io.stdout()).toContain('SOURCE');
|
||||
|
|
@ -1884,12 +1884,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('prints report-file JSON without looking up local ingest status', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const reportFile = await writeBundleReportFile(tempDir);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
|
||||
runKtxIngest({ command: 'status', projectDir, runId: 'report-1', reportFile, outputMode: 'json' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
const parsed = JSON.parse(io.stdout());
|
||||
|
|
@ -1905,13 +1905,13 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('routes interactive report-file replay through the stored TUI renderer', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const reportFile = await writeBundleReportFile(tempDir);
|
||||
const io = makeIo({ isTTY: true, stdinIsTTY: true, columns: 120 });
|
||||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -1937,12 +1937,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('rejects report-file replay when the requested id does not match the report', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const reportFile = await writeBundleReportFile(tempDir);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKloIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
|
||||
runKtxIngest({ command: 'replay', projectDir, runId: 'unrelated-id', reportFile, outputMode: 'plain' }, io.io),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain(
|
||||
|
|
@ -1953,7 +1953,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('renders memory-flow snapshot for status --viz when stdout is interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1963,13 +1963,13 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: true });
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{ command: 'status', projectDir, runId: 'viz-run-1', outputMode: 'viz', inputMode: 'disabled' },
|
||||
io.io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('SOURCE');
|
||||
expect(io.stdout()).toContain('CHUNKS');
|
||||
expect(io.stdout()).toContain('WORKUNITS');
|
||||
|
|
@ -1979,7 +1979,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('uses the TUI renderer for stored status --viz when stdin and stdout are interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1991,7 +1991,7 @@ describe('runKloIngest', () => {
|
|||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'status',
|
||||
projectDir,
|
||||
|
|
@ -2015,7 +2015,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('falls back to the text renderer when TUI declines stored status --viz', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2027,7 +2027,7 @@ describe('runKloIngest', () => {
|
|||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => false);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'status',
|
||||
projectDir,
|
||||
|
|
@ -2040,12 +2040,12 @@ describe('runKloIngest', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('does not use TUI for stored --viz when input is disabled', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2057,7 +2057,7 @@ describe('runKloIngest', () => {
|
|||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -2071,12 +2071,12 @@ describe('runKloIngest', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('falls back to plain status for stored --viz when stdin raw mode is unavailable', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2088,7 +2088,7 @@ describe('runKloIngest', () => {
|
|||
const renderStoredMemoryFlow = vi.fn(async (_input: MemoryFlowReplayInput, _io: unknown) => true);
|
||||
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -2103,7 +2103,7 @@ describe('runKloIngest', () => {
|
|||
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('Run: run-raw-missing-stored-viz-run');
|
||||
expect(io.stdout()).toContain('Job: raw-missing-stored-viz-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -2111,7 +2111,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('keeps stored --viz snapshot-only when input is disabled', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2121,7 +2121,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: true, columns: 120 });
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -2133,14 +2133,14 @@ describe('runKloIngest', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('keeps disabled-input stored --viz snapshot output even when stdin raw mode is unavailable', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2150,7 +2150,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: true, stdinIsTTY: true, rawMode: false, columns: 120 });
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -2162,14 +2162,14 @@ describe('runKloIngest', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KLO memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('degrades stored --viz snapshots to plain status when stdout is redirected even when input is disabled', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2179,7 +2179,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: false });
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{
|
||||
command: 'replay',
|
||||
projectDir,
|
||||
|
|
@ -2193,7 +2193,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Run: run-redirected-no-input-viz-run');
|
||||
expect(io.stdout()).toContain('Job: redirected-no-input-viz-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -2201,7 +2201,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('degrades ingest replay --viz to plain status when TERM is dumb', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2211,7 +2211,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: true });
|
||||
await expect(
|
||||
runKloIngest(
|
||||
runKtxIngest(
|
||||
{ command: 'replay', projectDir, runId: 'dumb-terminal-viz-run', outputMode: 'viz' },
|
||||
io.io,
|
||||
{ env: { ...process.env, TERM: 'dumb' } },
|
||||
|
|
@ -2220,7 +2220,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Run: run-dumb-terminal-viz-run');
|
||||
expect(io.stdout()).toContain('Job: dumb-terminal-viz-run');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but TERM=dumb does not support the visual renderer; printing plain output.',
|
||||
);
|
||||
|
|
@ -2228,7 +2228,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('falls back to plain status for --viz when stdout is not interactive', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2238,12 +2238,12 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo({ isTTY: false });
|
||||
await expect(
|
||||
runKloIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
|
||||
runKtxIngest({ command: 'replay', projectDir, runId: 'viz-run-2', outputMode: 'viz' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Run: run-viz-run-2');
|
||||
expect(io.stdout()).toContain('Job: viz-run-2');
|
||||
expect(io.stdout()).not.toContain('KLO memory flow');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -2251,7 +2251,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
it('prints JSON for status --json', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -2261,7 +2261,7 @@ describe('runKloIngest', () => {
|
|||
|
||||
const io = makeIo();
|
||||
await expect(
|
||||
runKloIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
|
||||
runKtxIngest({ command: 'status', projectDir, runId: 'json-run-1', outputMode: 'json' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(JSON.parse(io.stdout())).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import {
|
|||
renderMemoryFlowReplay,
|
||||
runLocalIngest,
|
||||
runLocalMetabaseIngest,
|
||||
} from '@klo/context/ingest';
|
||||
import { loadKloProject } from '@klo/context/project';
|
||||
} from '@ktx/context/ingest';
|
||||
import { loadKtxProject } from '@ktx/context/project';
|
||||
import { readIngestReportSnapshotFile } from './ingest-report-file.js';
|
||||
import { createKloCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { type KloMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
|
||||
import { createKtxCliLocalIngestAdapters } from './local-adapters.js';
|
||||
import { type KtxMemoryFlowStdin, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
|
||||
import {
|
||||
type KloMemoryFlowTuiIo,
|
||||
type KtxMemoryFlowTuiIo,
|
||||
type MemoryFlowTuiLiveSession,
|
||||
renderMemoryFlowTui,
|
||||
startLiveMemoryFlowTui,
|
||||
|
|
@ -29,10 +29,10 @@ import { profileMark } from './startup-profile.js';
|
|||
|
||||
profileMark('module:ingest');
|
||||
|
||||
export type KloIngestOutputMode = 'plain' | 'json' | 'viz';
|
||||
type KloIngestInputMode = 'auto' | 'disabled';
|
||||
export type KtxIngestOutputMode = 'plain' | 'json' | 'viz';
|
||||
type KtxIngestInputMode = 'auto' | 'disabled';
|
||||
|
||||
export type KloIngestArgs =
|
||||
export type KtxIngestArgs =
|
||||
| {
|
||||
command: 'run';
|
||||
projectDir: string;
|
||||
|
|
@ -41,28 +41,28 @@ export type KloIngestArgs =
|
|||
sourceDir?: string;
|
||||
databaseIntrospectionUrl?: string;
|
||||
debugLlmRequestFile?: string;
|
||||
outputMode: KloIngestOutputMode;
|
||||
inputMode?: KloIngestInputMode;
|
||||
outputMode: KtxIngestOutputMode;
|
||||
inputMode?: KtxIngestInputMode;
|
||||
}
|
||||
| {
|
||||
command: 'status' | 'replay' | 'watch';
|
||||
projectDir: string;
|
||||
runId?: string;
|
||||
reportFile?: string;
|
||||
outputMode: KloIngestOutputMode;
|
||||
inputMode?: KloIngestInputMode;
|
||||
outputMode: KtxIngestOutputMode;
|
||||
inputMode?: KtxIngestInputMode;
|
||||
};
|
||||
|
||||
interface KloIngestIo {
|
||||
stdin?: KloMemoryFlowStdin;
|
||||
interface KtxIngestIo {
|
||||
stdin?: KtxMemoryFlowStdin;
|
||||
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
}
|
||||
|
||||
interface KloIngestDeps {
|
||||
interface KtxIngestDeps {
|
||||
jobIdFactory?: () => string;
|
||||
now?: () => Date;
|
||||
createAdapters?: typeof createKloCliLocalIngestAdapters;
|
||||
createAdapters?: typeof createKtxCliLocalIngestAdapters;
|
||||
runLocalIngest?: typeof runLocalIngest;
|
||||
runLocalMetabaseIngest?: typeof runLocalMetabaseIngest;
|
||||
readReportFile?: typeof readIngestReportSnapshotFile;
|
||||
|
|
@ -93,7 +93,7 @@ function reportActionCounts(report: IngestReportSnapshot): { wikiCount: number;
|
|||
};
|
||||
}
|
||||
|
||||
function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void {
|
||||
function writeReportStatus(report: IngestReportSnapshot, io: KtxIngestIo): void {
|
||||
const counts = reportActionCounts(report);
|
||||
io.stdout.write(`Report: ${report.id}\n`);
|
||||
io.stdout.write(`Run: ${report.runId}\n`);
|
||||
|
|
@ -110,7 +110,7 @@ function writeReportStatus(report: IngestReportSnapshot, io: KloIngestIo): void
|
|||
io.stdout.write(`Provenance rows: ${report.body.provenanceRows.length}\n`);
|
||||
}
|
||||
|
||||
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KloIngestIo): void {
|
||||
function writeMetabaseFanoutStatus(result: LocalMetabaseFanoutResult, io: KtxIngestIo): void {
|
||||
io.stdout.write(`Metabase fan-out: ${result.status}\n`);
|
||||
io.stdout.write(`Source: ${result.metabaseConnectionId}\n`);
|
||||
io.stdout.write(`Children: ${result.children.length}\n`);
|
||||
|
|
@ -132,7 +132,7 @@ function pluralize(count: number, singular: string, plural = `${singular}s`): st
|
|||
|
||||
function createMetabaseFanoutProgress(
|
||||
connectionId: string,
|
||||
io: KloIngestIo,
|
||||
io: KtxIngestIo,
|
||||
): LocalMetabaseFanoutProgress {
|
||||
io.stdout.write(`Metabase ingest: ${connectionId}\n`);
|
||||
io.stdout.write('Checking mappings and scheduled-pull targets...\n');
|
||||
|
|
@ -156,7 +156,7 @@ function createMetabaseFanoutProgress(
|
|||
};
|
||||
}
|
||||
|
||||
function writeReportJson(report: IngestReportSnapshot, io: KloIngestIo): void {
|
||||
function writeReportJson(report: IngestReportSnapshot, io: KtxIngestIo): void {
|
||||
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
|
|
@ -172,21 +172,21 @@ function assertReportMatchesReplayId(report: IngestReportSnapshot, requestedId:
|
|||
}
|
||||
|
||||
async function readStoredIngestReport(
|
||||
project: Awaited<ReturnType<typeof loadKloProject>>,
|
||||
project: Awaited<ReturnType<typeof loadKtxProject>>,
|
||||
runId: string | undefined,
|
||||
): Promise<IngestReportSnapshot | null> {
|
||||
return runId ? await getLocalIngestStatus(project, runId) : await getLatestLocalIngestStatus(project);
|
||||
}
|
||||
|
||||
function isInteractiveTerminal(io: KloIngestIo): boolean {
|
||||
function isInteractiveTerminal(io: KtxIngestIo): boolean {
|
||||
return io.stdout.isTTY === true;
|
||||
}
|
||||
|
||||
function terminalWidth(io: KloIngestIo): number | undefined {
|
||||
function terminalWidth(io: KtxIngestIo): number | undefined {
|
||||
return io.stdout.columns ?? process.stdout.columns;
|
||||
}
|
||||
|
||||
function isTuiCapableIo(io: KloIngestIo): io is KloIngestIo & KloMemoryFlowTuiIo {
|
||||
function isTuiCapableIo(io: KtxIngestIo): io is KtxIngestIo & KtxMemoryFlowTuiIo {
|
||||
return (
|
||||
io.stdin?.isTTY === true &&
|
||||
io.stdout.isTTY === true &&
|
||||
|
|
@ -201,11 +201,11 @@ interface EffectiveIngestOutputModeOptions {
|
|||
}
|
||||
|
||||
function effectiveIngestOutputMode(
|
||||
outputMode: KloIngestOutputMode,
|
||||
io: KloIngestIo,
|
||||
outputMode: KtxIngestOutputMode,
|
||||
io: KtxIngestIo,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: EffectiveIngestOutputModeOptions = {},
|
||||
): KloIngestOutputMode {
|
||||
): KtxIngestOutputMode {
|
||||
if (outputMode !== 'viz') {
|
||||
return outputMode;
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ function effectiveIngestOutputMode(
|
|||
return 'plain';
|
||||
}
|
||||
|
||||
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, options: { clear?: boolean } = {}): void {
|
||||
function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KtxIngestIo, options: { clear?: boolean } = {}): void {
|
||||
if (options.clear) {
|
||||
io.stdout.write('\u001b[2J\u001b[H');
|
||||
}
|
||||
|
|
@ -228,7 +228,7 @@ function writeMemoryFlowInput(input: MemoryFlowReplayInput, io: KloIngestIo, opt
|
|||
}
|
||||
|
||||
function initialRunMemoryFlowInput(
|
||||
args: Extract<KloIngestArgs, { command: 'run' }>,
|
||||
args: Extract<KtxIngestArgs, { command: 'run' }>,
|
||||
runId: string,
|
||||
): MemoryFlowReplayInput {
|
||||
return {
|
||||
|
|
@ -247,8 +247,8 @@ function initialRunMemoryFlowInput(
|
|||
|
||||
async function writeReportRecord(
|
||||
report: IngestReportSnapshot,
|
||||
outputMode: KloIngestOutputMode,
|
||||
io: KloIngestIo,
|
||||
outputMode: KtxIngestOutputMode,
|
||||
io: KtxIngestIo,
|
||||
options: {
|
||||
interactive?: boolean;
|
||||
renderStoredMemoryFlow?: typeof renderMemoryFlowTui;
|
||||
|
|
@ -288,16 +288,16 @@ async function writeReportRecord(
|
|||
writeReportStatus(report, io);
|
||||
}
|
||||
|
||||
export async function runKloIngest(
|
||||
args: KloIngestArgs,
|
||||
io: KloIngestIo = process,
|
||||
deps: KloIngestDeps = {},
|
||||
export async function runKtxIngest(
|
||||
args: KtxIngestArgs,
|
||||
io: KtxIngestIo = process,
|
||||
deps: KtxIngestDeps = {},
|
||||
): Promise<number> {
|
||||
try {
|
||||
const project = await loadKloProject({ projectDir: args.projectDir });
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const env = deps.env ?? process.env;
|
||||
if (args.command === 'run') {
|
||||
const createAdapters = deps.createAdapters ?? createKloCliLocalIngestAdapters;
|
||||
const createAdapters = deps.createAdapters ?? createKtxCliLocalIngestAdapters;
|
||||
const executeLocalIngest = deps.runLocalIngest ?? runLocalIngest;
|
||||
const localIngestOptions = deps.localIngestOptions ?? {};
|
||||
const adapterOptions = {
|
||||
|
|
@ -409,7 +409,7 @@ export async function runKloIngest(
|
|||
throw new Error(
|
||||
args.runId
|
||||
? `Local ingest run or report "${args.runId}" was not found`
|
||||
: 'No local ingest reports were found. Run `klo ingest --all` first.',
|
||||
: 'No local ingest reports were found. Run `ktx ingest --all` first.',
|
||||
);
|
||||
}
|
||||
await writeReportRecord(report, args.outputMode, io, {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { KloCliIo } from '../cli-runtime.js';
|
||||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
import { resolveOutputMode } from './mode.js';
|
||||
|
||||
function ioWith(isTTY: boolean | undefined): KloCliIo {
|
||||
function ioWith(isTTY: boolean | undefined): KtxCliIo {
|
||||
return {
|
||||
stdout: { isTTY, write: () => {} },
|
||||
stderr: { write: () => {} },
|
||||
|
|
@ -24,14 +24,14 @@ describe('resolveOutputMode', () => {
|
|||
expect(() => resolveOutputMode({ explicit: 'fancy', io: ioWith(true), env: {} })).toThrow(/Invalid --output/);
|
||||
});
|
||||
|
||||
it('honors KLO_OUTPUT env var when no explicit value', () => {
|
||||
expect(resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('plain');
|
||||
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'pretty' } })).toBe('pretty');
|
||||
expect(resolveOutputMode({ io: ioWith(false), env: { KLO_OUTPUT: 'json' } })).toBe('json');
|
||||
it('honors KTX_OUTPUT env var when no explicit value', () => {
|
||||
expect(resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('plain');
|
||||
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'pretty' } })).toBe('pretty');
|
||||
expect(resolveOutputMode({ io: ioWith(false), env: { KTX_OUTPUT: 'json' } })).toBe('json');
|
||||
});
|
||||
|
||||
it('throws on unknown KLO_OUTPUT', () => {
|
||||
expect(() => resolveOutputMode({ io: ioWith(true), env: { KLO_OUTPUT: 'fancy' } })).toThrow(/Invalid KLO_OUTPUT/);
|
||||
it('throws on unknown KTX_OUTPUT', () => {
|
||||
expect(() => resolveOutputMode({ io: ioWith(true), env: { KTX_OUTPUT: 'fancy' } })).toThrow(/Invalid KTX_OUTPUT/);
|
||||
});
|
||||
|
||||
it('returns plain when CI is set to a truthy value', () => {
|
||||
|
|
@ -54,7 +54,7 @@ describe('resolveOutputMode', () => {
|
|||
expect(resolveOutputMode({ io: ioWith(undefined), env: {} })).toBe('plain');
|
||||
});
|
||||
|
||||
it('explicit value beats KLO_OUTPUT env var', () => {
|
||||
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KLO_OUTPUT: 'plain' } })).toBe('json');
|
||||
it('explicit value beats KTX_OUTPUT env var', () => {
|
||||
expect(resolveOutputMode({ explicit: 'json', io: ioWith(true), env: { KTX_OUTPUT: 'plain' } })).toBe('json');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import type { KloCliIo } from '../cli-runtime.js';
|
||||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
|
||||
export type KloOutputMode = 'pretty' | 'plain' | 'json';
|
||||
export type KtxOutputMode = 'pretty' | 'plain' | 'json';
|
||||
|
||||
const MODES: ReadonlySet<string> = new Set(['pretty', 'plain', 'json']);
|
||||
|
||||
export interface ResolveOutputModeArgs {
|
||||
explicit?: string;
|
||||
json?: boolean;
|
||||
io: KloCliIo;
|
||||
io: KtxCliIo;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
|
||||
export function resolveOutputMode(args: ResolveOutputModeArgs): KtxOutputMode {
|
||||
if (args.json === true) {
|
||||
return 'json';
|
||||
}
|
||||
|
|
@ -19,15 +19,15 @@ export function resolveOutputMode(args: ResolveOutputModeArgs): KloOutputMode {
|
|||
if (!MODES.has(args.explicit)) {
|
||||
throw new Error(`Invalid --output value: ${args.explicit}. Expected one of pretty, plain, json.`);
|
||||
}
|
||||
return args.explicit as KloOutputMode;
|
||||
return args.explicit as KtxOutputMode;
|
||||
}
|
||||
const env = args.env ?? process.env;
|
||||
const envMode = env.KLO_OUTPUT;
|
||||
const envMode = env.KTX_OUTPUT;
|
||||
if (envMode !== undefined && envMode !== '') {
|
||||
if (!MODES.has(envMode)) {
|
||||
throw new Error(`Invalid KLO_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
|
||||
throw new Error(`Invalid KTX_OUTPUT value: ${envMode}. Expected one of pretty, plain, json.`);
|
||||
}
|
||||
return envMode as KloOutputMode;
|
||||
return envMode as KtxOutputMode;
|
||||
}
|
||||
const ci = env.CI;
|
||||
if (ci !== undefined && ci !== '' && ci !== '0' && ci !== 'false') {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { KloCliIo } from '../cli-runtime.js';
|
||||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
import { printList, type PrintListColumn } from './print-list.js';
|
||||
import { SYMBOLS } from './symbols.js';
|
||||
|
||||
function recorder(): { io: KloCliIo; out: () => string; err: () => string } {
|
||||
function recorder(): { io: KtxCliIo; out: () => string; err: () => string } {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { KloCliIo } from '../cli-runtime.js';
|
||||
import type { KloOutputMode } from './mode.js';
|
||||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
import type { KtxOutputMode } from './mode.js';
|
||||
import { bold, dim, SYMBOLS } from './symbols.js';
|
||||
|
||||
export interface PrintListColumn<Row> {
|
||||
|
|
@ -24,8 +24,8 @@ export interface PrintListArgs<Row> {
|
|||
groupBy?: keyof Row & string;
|
||||
emptyMessage: string;
|
||||
command: string;
|
||||
mode: KloOutputMode;
|
||||
io: KloCliIo;
|
||||
mode: KtxOutputMode;
|
||||
io: KtxCliIo;
|
||||
}
|
||||
|
||||
export function printList<Row extends object>(args: PrintListArgs<Row>): void {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKloProject } from '@klo/context/project';
|
||||
import { initKtxProject } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { runKloKnowledge } from './knowledge.js';
|
||||
import { runKtxKnowledge } from './knowledge.js';
|
||||
|
||||
function makeIo() {
|
||||
let stdout = '';
|
||||
|
|
@ -26,11 +26,11 @@ function makeIo() {
|
|||
};
|
||||
}
|
||||
|
||||
describe('runKloKnowledge', () => {
|
||||
describe('runKtxKnowledge', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-knowledge-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-knowledge-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -39,11 +39,11 @@ describe('runKloKnowledge', () => {
|
|||
|
||||
it('writes, reads, lists, and searches knowledge pages', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
const writeIo = makeIo();
|
||||
await expect(
|
||||
runKloKnowledge(
|
||||
runKtxKnowledge(
|
||||
{
|
||||
command: 'write',
|
||||
projectDir,
|
||||
|
|
@ -63,33 +63,33 @@ describe('runKloKnowledge', () => {
|
|||
|
||||
const readIo = makeIo();
|
||||
await expect(
|
||||
runKloKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
|
||||
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics/revenue', userId: 'local' }, readIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(readIo.stdout()).toContain('# metrics/revenue');
|
||||
expect(readIo.stdout()).toContain('Revenue is paid order value.');
|
||||
|
||||
const listIo = makeIo();
|
||||
await expect(runKloKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
|
||||
expect(listIo.stdout()).toContain('GLOBAL\tmetrics/revenue\tRevenue');
|
||||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
runKloKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
|
||||
runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(searchIo.stdout()).toContain('metrics/revenue');
|
||||
});
|
||||
|
||||
it('explains empty search results for a project without wiki pages', async () => {
|
||||
const projectDir = join(tempDir, 'empty-project');
|
||||
await initKloProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
runKloKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
|
||||
runKtxKnowledge({ command: 'search', projectDir, query: 'revenue', userId: 'local' }, searchIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(searchIo.stdout()).toBe('');
|
||||
expect(searchIo.stderr()).toContain('No local wiki pages found');
|
||||
expect(searchIo.stderr()).toContain('klo wiki write');
|
||||
expect(searchIo.stderr()).toContain('ktx wiki write');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { loadKloProject } from '@klo/context/project';
|
||||
import { loadKtxProject } from '@ktx/context/project';
|
||||
import {
|
||||
type LocalKnowledgeScope,
|
||||
listLocalKnowledgePages,
|
||||
readLocalKnowledgePage,
|
||||
searchLocalKnowledgePages,
|
||||
writeLocalKnowledgePage,
|
||||
} from '@klo/context/wiki';
|
||||
} from '@ktx/context/wiki';
|
||||
|
||||
export type KloKnowledgeArgs =
|
||||
export type KtxKnowledgeArgs =
|
||||
| { command: 'list'; projectDir: string; userId: string }
|
||||
| { command: 'read'; projectDir: string; key: string; userId: string }
|
||||
| { command: 'search'; projectDir: string; query: string; userId: string }
|
||||
|
|
@ -24,14 +24,14 @@ export type KloKnowledgeArgs =
|
|||
slRefs: string[];
|
||||
};
|
||||
|
||||
interface KloKnowledgeIo {
|
||||
interface KtxKnowledgeIo {
|
||||
stdout: { write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
}
|
||||
|
||||
export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo = process): Promise<number> {
|
||||
export async function runKtxKnowledge(args: KtxKnowledgeArgs, io: KtxKnowledgeIo = process): Promise<number> {
|
||||
try {
|
||||
const project = await loadKloProject({ projectDir: args.projectDir });
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
if (args.command === 'list') {
|
||||
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
|
||||
for (const page of pages) {
|
||||
|
|
@ -56,11 +56,11 @@ export async function runKloKnowledge(args: KloKnowledgeArgs, io: KloKnowledgeIo
|
|||
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 \`klo wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
|
||||
`No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write <key> --summary <summary> --content <content>\` or run ingest.\n`,
|
||||
);
|
||||
} else {
|
||||
io.stderr.write(
|
||||
`No local wiki pages matched "${args.query}". Run \`klo wiki list\` to inspect available pages.\n`,
|
||||
`No local wiki pages matched "${args.query}". Run \`ktx wiki list\` to inspect available pages.\n`,
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { join } from 'node:path';
|
||||
import { createBigQueryLiveDatabaseIntrospection, isKloBigQueryConnectionConfig } from '@klo/connector-bigquery';
|
||||
import { createClickHouseLiveDatabaseIntrospection, isKloClickHouseConnectionConfig } from '@klo/connector-clickhouse';
|
||||
import { createMysqlLiveDatabaseIntrospection, isKloMysqlConnectionConfig } from '@klo/connector-mysql';
|
||||
import { createBigQueryLiveDatabaseIntrospection, isKtxBigQueryConnectionConfig } from '@ktx/connector-bigquery';
|
||||
import { createClickHouseLiveDatabaseIntrospection, isKtxClickHouseConnectionConfig } from '@ktx/connector-clickhouse';
|
||||
import { createMysqlLiveDatabaseIntrospection, isKtxMysqlConnectionConfig } from '@ktx/connector-mysql';
|
||||
import {
|
||||
createPostgresLiveDatabaseIntrospection,
|
||||
isKloPostgresConnectionConfig,
|
||||
type KloPostgresConnectionConfig,
|
||||
KloPostgresHistoricSqlQueryClient,
|
||||
} from '@klo/connector-postgres';
|
||||
import { createSqliteLiveDatabaseIntrospection, isKloSqliteConnectionConfig } from '@klo/connector-sqlite';
|
||||
import { createSqlServerLiveDatabaseIntrospection, isKloSqlServerConnectionConfig } from '@klo/connector-sqlserver';
|
||||
isKtxPostgresConnectionConfig,
|
||||
type KtxPostgresConnectionConfig,
|
||||
KtxPostgresHistoricSqlQueryClient,
|
||||
} from '@ktx/connector-postgres';
|
||||
import { createSqliteLiveDatabaseIntrospection, isKtxSqliteConnectionConfig } from '@ktx/connector-sqlite';
|
||||
import { createSqlServerLiveDatabaseIntrospection, isKtxSqlServerConnectionConfig } from '@ktx/connector-sqlserver';
|
||||
import {
|
||||
createDaemonLiveDatabaseIntrospection,
|
||||
createDefaultLocalIngestAdapters,
|
||||
|
|
@ -17,9 +17,9 @@ import {
|
|||
type LiveDatabaseIntrospectionPort,
|
||||
LiveDatabaseSourceAdapter,
|
||||
type SourceAdapter,
|
||||
} from '@klo/context/ingest';
|
||||
import type { KloLocalProject } from '@klo/context/project';
|
||||
import { createHttpSqlAnalysisPort } from '@klo/context/sql-analysis';
|
||||
} from '@ktx/context/ingest';
|
||||
import type { KtxLocalProject } from '@ktx/context/project';
|
||||
import { createHttpSqlAnalysisPort } from '@ktx/context/sql-analysis';
|
||||
|
||||
function hasSnowflakeDriver(connection: unknown): boolean {
|
||||
return (
|
||||
|
|
@ -29,8 +29,8 @@ function hasSnowflakeDriver(connection: unknown): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function createKloCliLiveDatabaseIntrospection(
|
||||
project: KloLocalProject,
|
||||
function createKtxCliLiveDatabaseIntrospection(
|
||||
project: KtxLocalProject,
|
||||
options: DefaultLocalIngestAdaptersOptions = {},
|
||||
): LiveDatabaseIntrospectionPort {
|
||||
const daemon = createDaemonLiveDatabaseIntrospection({
|
||||
|
|
@ -60,29 +60,29 @@ function createKloCliLiveDatabaseIntrospection(
|
|||
return {
|
||||
async extractSchema(connectionId: string) {
|
||||
const connection = project.config.connections[connectionId];
|
||||
if (isKloPostgresConnectionConfig(connection)) {
|
||||
if (isKtxPostgresConnectionConfig(connection)) {
|
||||
return postgres.extractSchema(connectionId);
|
||||
}
|
||||
if (isKloSqliteConnectionConfig(connection)) {
|
||||
if (isKtxSqliteConnectionConfig(connection)) {
|
||||
return sqlite.extractSchema(connectionId);
|
||||
}
|
||||
if (isKloMysqlConnectionConfig(connection)) {
|
||||
if (isKtxMysqlConnectionConfig(connection)) {
|
||||
return mysql.extractSchema(connectionId);
|
||||
}
|
||||
if (isKloClickHouseConnectionConfig(connection)) {
|
||||
if (isKtxClickHouseConnectionConfig(connection)) {
|
||||
return clickhouse.extractSchema(connectionId);
|
||||
}
|
||||
if (isKloSqlServerConnectionConfig(connection)) {
|
||||
if (isKtxSqlServerConnectionConfig(connection)) {
|
||||
return sqlserver.extractSchema(connectionId);
|
||||
}
|
||||
if (isKloBigQueryConnectionConfig(connection)) {
|
||||
if (isKtxBigQueryConnectionConfig(connection)) {
|
||||
return bigquery.extractSchema(connectionId);
|
||||
}
|
||||
if (hasSnowflakeDriver(connection)) {
|
||||
const { createSnowflakeLiveDatabaseIntrospection, isKloSnowflakeConnectionConfig } = await import(
|
||||
'@klo/connector-snowflake'
|
||||
const { createSnowflakeLiveDatabaseIntrospection, isKtxSnowflakeConnectionConfig } = await import(
|
||||
'@ktx/connector-snowflake'
|
||||
);
|
||||
if (!isKloSnowflakeConnectionConfig(connection)) {
|
||||
if (!isKtxSnowflakeConnectionConfig(connection)) {
|
||||
return daemon.extractSchema(connectionId);
|
||||
}
|
||||
const snowflake = createSnowflakeLiveDatabaseIntrospection({
|
||||
|
|
@ -95,13 +95,13 @@ function createKloCliLiveDatabaseIntrospection(
|
|||
};
|
||||
}
|
||||
|
||||
interface KloCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
|
||||
interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
|
||||
historicSqlConnectionId?: string;
|
||||
sqlAnalysisUrl?: string;
|
||||
}
|
||||
|
||||
function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectionConfig | undefined): boolean {
|
||||
if (!connection || !isKloPostgresConnectionConfig(connection)) {
|
||||
function isEnabledPostgresHistoricSqlConnection(connection: KtxPostgresConnectionConfig | undefined): boolean {
|
||||
if (!connection || !isKtxPostgresConnectionConfig(connection)) {
|
||||
return false;
|
||||
}
|
||||
const historicSql =
|
||||
|
|
@ -113,16 +113,16 @@ function isEnabledPostgresHistoricSqlConnection(connection: KloPostgresConnectio
|
|||
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
|
||||
}
|
||||
|
||||
function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, connectionId: string) {
|
||||
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
|
||||
if (!isKloPostgresConnectionConfig(connection)) {
|
||||
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
|
||||
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
|
||||
if (!isKtxPostgresConnectionConfig(connection)) {
|
||||
throw new Error(
|
||||
`Historic SQL local ingest requires a Postgres connection, got ${String(connection?.driver ?? 'unknown')}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
async executeQuery(sql: string, params?: unknown[]) {
|
||||
const client = new KloPostgresHistoricSqlQueryClient({
|
||||
const client = new KtxPostgresHistoricSqlQueryClient({
|
||||
connectionId,
|
||||
connection,
|
||||
});
|
||||
|
|
@ -135,12 +135,12 @@ function createEphemeralPostgresHistoricSqlClient(project: KloLocalProject, conn
|
|||
};
|
||||
}
|
||||
|
||||
function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCliLocalIngestAdaptersOptions) {
|
||||
function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCliLocalIngestAdaptersOptions) {
|
||||
const connectionId = options.historicSqlConnectionId;
|
||||
if (!connectionId) {
|
||||
return undefined;
|
||||
}
|
||||
const connection = project.config.connections[connectionId] as KloPostgresConnectionConfig | undefined;
|
||||
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
|
||||
if (!isEnabledPostgresHistoricSqlConnection(connection)) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -148,18 +148,18 @@ function historicSqlOptionsForLocalRun(project: KloLocalProject, options: KloCli
|
|||
sqlAnalysis: createHttpSqlAnalysisPort({
|
||||
baseUrl:
|
||||
options.sqlAnalysisUrl ??
|
||||
process.env.KLO_SQL_ANALYSIS_URL ??
|
||||
process.env.KLO_DAEMON_URL ??
|
||||
process.env.KTX_SQL_ANALYSIS_URL ??
|
||||
process.env.KTX_DAEMON_URL ??
|
||||
'http://127.0.0.1:8765',
|
||||
}),
|
||||
postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
|
||||
postgresBaselineRootDir: join(project.projectDir, '.klo/cache/historic-sql'),
|
||||
postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
|
||||
};
|
||||
}
|
||||
|
||||
export function createKloCliLocalIngestAdapters(
|
||||
project: KloLocalProject,
|
||||
options: KloCliLocalIngestAdaptersOptions = {},
|
||||
export function createKtxCliLocalIngestAdapters(
|
||||
project: KtxLocalProject,
|
||||
options: KtxCliLocalIngestAdaptersOptions = {},
|
||||
): SourceAdapter[] {
|
||||
const historicSql = historicSqlOptionsForLocalRun(project, options);
|
||||
const base = createDefaultLocalIngestAdapters(project, {
|
||||
|
|
@ -167,7 +167,7 @@ export function createKloCliLocalIngestAdapters(
|
|||
...(historicSql ? { historicSql } : {}),
|
||||
});
|
||||
const liveDatabase = new LiveDatabaseSourceAdapter({
|
||||
introspection: createKloCliLiveDatabaseIntrospection(project, options),
|
||||
introspection: createKtxCliLiveDatabaseIntrospection(project, options),
|
||||
});
|
||||
return base.map((adapter) => (adapter.source === 'live-database' ? liveDatabase : adapter));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKloProject, loadKloProject } from '@klo/context/project';
|
||||
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createKloCliScanConnector } from './local-scan-connectors.js';
|
||||
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
||||
|
||||
const bigQueryMock = vi.hoisted(() => ({
|
||||
constructorInputs: [] as Array<{
|
||||
|
|
@ -13,10 +13,10 @@ const bigQueryMock = vi.hoisted(() => ({
|
|||
}>,
|
||||
}));
|
||||
|
||||
vi.mock('@klo/connector-bigquery', () => ({
|
||||
isKloBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
|
||||
vi.mock('@ktx/connector-bigquery', () => ({
|
||||
isKtxBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) =>
|
||||
String(connection?.driver ?? '').toLowerCase() === 'bigquery',
|
||||
KloBigQueryScanConnector: class {
|
||||
KtxBigQueryScanConnector: class {
|
||||
readonly id: string;
|
||||
readonly driver = 'bigquery';
|
||||
|
||||
|
|
@ -27,12 +27,12 @@ vi.mock('@klo/connector-bigquery', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
describe('createKloCliScanConnector', () => {
|
||||
describe('createKtxCliScanConnector', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
bigQueryMock.constructorInputs.length = 0;
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'klo-cli-scan-connector-'));
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-scan-connector-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -40,9 +40,9 @@ describe('createKloCliScanConnector', () => {
|
|||
});
|
||||
|
||||
it('creates a native sqlite connector from standalone config', async () => {
|
||||
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -54,9 +54,9 @@ describe('createKloCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir: tempDir });
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
const connector = await createKloCliScanConnector(project, 'warehouse');
|
||||
const connector = await createKtxCliScanConnector(project, 'warehouse');
|
||||
|
||||
expect(connector.id).toBe('sqlite:warehouse');
|
||||
expect(connector.driver).toBe('sqlite');
|
||||
|
|
@ -66,9 +66,9 @@ describe('createKloCliScanConnector', () => {
|
|||
['maxBytesBilled', ' maxBytesBilled: 123456789', 123456789],
|
||||
['max_bytes_billed', ' max_bytes_billed: "987654321"', '987654321'],
|
||||
])('passes BigQuery %s from standalone config', async (_label, byteCapLine, expectedMaxBytesBilled) => {
|
||||
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -81,9 +81,9 @@ describe('createKloCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir: tempDir });
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
const connector = await createKloCliScanConnector(project, 'warehouse');
|
||||
const connector = await createKtxCliScanConnector(project, 'warehouse');
|
||||
|
||||
expect(connector.id).toBe('bigquery:warehouse');
|
||||
expect(connector.driver).toBe('bigquery');
|
||||
|
|
@ -96,9 +96,9 @@ describe('createKloCliScanConnector', () => {
|
|||
});
|
||||
|
||||
it('does not create a standalone PostHog scan connector', async () => {
|
||||
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -111,17 +111,17 @@ describe('createKloCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir: tempDir });
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
await expect(createKloCliScanConnector(project, 'product')).rejects.toThrow(
|
||||
'Connection "product" uses driver "posthog", which has no native standalone KLO scan connector',
|
||||
await expect(createKtxCliScanConnector(project, 'product')).rejects.toThrow(
|
||||
'Connection "product" uses driver "posthog", which has no native standalone KTX scan connector',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for structural daemon-only fallback configs', async () => {
|
||||
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -132,17 +132,17 @@ describe('createKloCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir: tempDir });
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" uses driver "duckdb", which has no native standalone KLO scan connector',
|
||||
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" uses driver "duckdb", which has no native standalone KTX scan connector',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a clear error when the connection block has no driver field', async () => {
|
||||
await initKloProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await writeFile(
|
||||
join(tempDir, 'klo.yaml'),
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
|
|
@ -154,10 +154,10 @@ describe('createKloCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKloProject({ projectDir: tempDir });
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
await expect(createKloCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" has no `driver` field in klo.yaml',
|
||||
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" has no `driver` field in ktx.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { KloLocalProject } from '@klo/context/project';
|
||||
import type { KloScanConnector } from '@klo/context/scan';
|
||||
import type { KtxLocalProject } from '@ktx/context/project';
|
||||
import type { KtxScanConnector } from '@ktx/context/scan';
|
||||
|
||||
const SUPPORTED_DRIVERS = 'sqlite, postgres, mysql, clickhouse, sqlserver, bigquery, snowflake';
|
||||
|
||||
function bigQueryMaxBytesBilled(
|
||||
connection: KloLocalProject['config']['connections'][string],
|
||||
connection: KtxLocalProject['config']['connections'][string],
|
||||
): number | string | undefined {
|
||||
const raw = connection.maxBytesBilled ?? connection.max_bytes_billed;
|
||||
if (typeof raw === 'number') {
|
||||
|
|
@ -17,55 +17,55 @@ function bigQueryMaxBytesBilled(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export async function createKloCliScanConnector(
|
||||
project: KloLocalProject,
|
||||
export async function createKtxCliScanConnector(
|
||||
project: KtxLocalProject,
|
||||
connectionId: string,
|
||||
): Promise<KloScanConnector> {
|
||||
): Promise<KtxScanConnector> {
|
||||
const connection = project.config.connections[connectionId];
|
||||
if (!connection) {
|
||||
throw new Error(`Connection "${connectionId}" is not configured in klo.yaml`);
|
||||
throw new Error(`Connection "${connectionId}" is not configured in ktx.yaml`);
|
||||
}
|
||||
const driver = String(connection.driver ?? '').toLowerCase();
|
||||
if (!driver) {
|
||||
throw new Error(
|
||||
`Connection "${connectionId}" has no \`driver\` field in klo.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
|
||||
`Connection "${connectionId}" has no \`driver\` field in ktx.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`,
|
||||
);
|
||||
}
|
||||
if (driver === 'sqlite' || driver === 'sqlite3') {
|
||||
const { KloSqliteScanConnector, isKloSqliteConnectionConfig } = await import('@klo/connector-sqlite');
|
||||
if (isKloSqliteConnectionConfig(connection)) {
|
||||
return new KloSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
|
||||
const { KtxSqliteScanConnector, isKtxSqliteConnectionConfig } = await import('@ktx/connector-sqlite');
|
||||
if (isKtxSqliteConnectionConfig(connection)) {
|
||||
return new KtxSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir });
|
||||
}
|
||||
}
|
||||
if (driver === 'postgres' || driver === 'postgresql') {
|
||||
const { KloPostgresScanConnector, isKloPostgresConnectionConfig } = await import('@klo/connector-postgres');
|
||||
if (isKloPostgresConnectionConfig(connection)) {
|
||||
return new KloPostgresScanConnector({ connectionId, connection });
|
||||
const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('@ktx/connector-postgres');
|
||||
if (isKtxPostgresConnectionConfig(connection)) {
|
||||
return new KtxPostgresScanConnector({ connectionId, connection });
|
||||
}
|
||||
}
|
||||
if (driver === 'mysql') {
|
||||
const { KloMysqlScanConnector, isKloMysqlConnectionConfig } = await import('@klo/connector-mysql');
|
||||
if (isKloMysqlConnectionConfig(connection)) {
|
||||
return new KloMysqlScanConnector({ connectionId, connection });
|
||||
const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('@ktx/connector-mysql');
|
||||
if (isKtxMysqlConnectionConfig(connection)) {
|
||||
return new KtxMysqlScanConnector({ connectionId, connection });
|
||||
}
|
||||
}
|
||||
if (driver === 'clickhouse') {
|
||||
const { KloClickHouseScanConnector, isKloClickHouseConnectionConfig } = await import('@klo/connector-clickhouse');
|
||||
if (isKloClickHouseConnectionConfig(connection)) {
|
||||
return new KloClickHouseScanConnector({ connectionId, connection });
|
||||
const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('@ktx/connector-clickhouse');
|
||||
if (isKtxClickHouseConnectionConfig(connection)) {
|
||||
return new KtxClickHouseScanConnector({ connectionId, connection });
|
||||
}
|
||||
}
|
||||
if (driver === 'sqlserver') {
|
||||
const { KloSqlServerScanConnector, isKloSqlServerConnectionConfig } = await import('@klo/connector-sqlserver');
|
||||
if (isKloSqlServerConnectionConfig(connection)) {
|
||||
return new KloSqlServerScanConnector({ connectionId, connection });
|
||||
const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('@ktx/connector-sqlserver');
|
||||
if (isKtxSqlServerConnectionConfig(connection)) {
|
||||
return new KtxSqlServerScanConnector({ connectionId, connection });
|
||||
}
|
||||
}
|
||||
if (driver === 'bigquery') {
|
||||
const { KloBigQueryScanConnector, isKloBigQueryConnectionConfig } = await import('@klo/connector-bigquery');
|
||||
if (isKloBigQueryConnectionConfig(connection)) {
|
||||
const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('@ktx/connector-bigquery');
|
||||
if (isKtxBigQueryConnectionConfig(connection)) {
|
||||
const maxBytesBilled = bigQueryMaxBytesBilled(connection);
|
||||
return new KloBigQueryScanConnector({
|
||||
return new KtxBigQueryScanConnector({
|
||||
connectionId,
|
||||
connection,
|
||||
...(maxBytesBilled !== undefined ? { maxBytesBilled } : {}),
|
||||
|
|
@ -73,12 +73,12 @@ export async function createKloCliScanConnector(
|
|||
}
|
||||
}
|
||||
if (driver === 'snowflake') {
|
||||
const { KloSnowflakeScanConnector, isKloSnowflakeConnectionConfig } = await import('@klo/connector-snowflake');
|
||||
if (isKloSnowflakeConnectionConfig(connection)) {
|
||||
return new KloSnowflakeScanConnector({ connectionId, connection });
|
||||
const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('@ktx/connector-snowflake');
|
||||
if (isKtxSnowflakeConnectionConfig(connection)) {
|
||||
return new KtxSnowflakeScanConnector({ connectionId, connection });
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KLO scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
|
||||
`Connection "${connectionId}" uses driver "${driver}", which has no native standalone KTX scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* @jsxImportSource react */
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@klo/context/ingest/memory-flow';
|
||||
import type { MemoryFlowEvent, MemoryFlowReplayInput } from '@ktx/context/ingest/memory-flow';
|
||||
import { Box, Text } from 'ink';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { buildDemoMetrics, formatCost, formatDuration } from './demo-metrics.js';
|
||||
|
|
@ -315,7 +315,7 @@ function pad(str: string, width: number): string {
|
|||
return str.length >= width ? str : str + ' '.repeat(width - str.length);
|
||||
}
|
||||
|
||||
const KLO_LOGO_SMALL = [
|
||||
const KTX_LOGO_SMALL = [
|
||||
'██╗ ██╗██╗ ██████╗ ',
|
||||
'██║ ██╔╝██║ ██╔═══██╗',
|
||||
'█████╔╝ ██║ ██║ ██║',
|
||||
|
|
@ -328,7 +328,7 @@ export function Logo(props: { theme: HudTheme; done: boolean }): ReactNode {
|
|||
const color = props.done ? props.theme.complete : props.theme.active;
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} paddingLeft={2}>
|
||||
{KLO_LOGO_SMALL.map((line, idx) => (
|
||||
{KTX_LOGO_SMALL.map((line, idx) => (
|
||||
<Text key={idx} color={color}>
|
||||
{line}
|
||||
</Text>
|
||||
|
|
@ -492,7 +492,7 @@ export function ActivityFeed(props: {
|
|||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results — what KLO has created */}
|
||||
{/* Results — what KTX has created */}
|
||||
{insights.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={props.theme.text}> Created so far:</Text>
|
||||
|
|
@ -524,7 +524,7 @@ export function ActivityFeed(props: {
|
|||
<Text color={props.theme.active}>{spinner(props.frame)} Saving to context layer...</Text>
|
||||
)}
|
||||
{savedEvent && (
|
||||
<Text color={props.theme.complete}>✓ Saved — your agents can now use the KLO context layer</Text>
|
||||
<Text color={props.theme.complete}>✓ Saved — your agents can now use the KTX context layer</Text>
|
||||
)}
|
||||
|
||||
{/* Phase 7: Completion */}
|
||||
|
|
@ -559,12 +559,12 @@ function CompletionSummary(props: {
|
|||
<>
|
||||
<Text color={props.theme.border}>{'─'.repeat(60)}</Text>
|
||||
<Text bold color={props.theme.complete}>
|
||||
★ KLO finished ingesting your data
|
||||
★ KTX finished ingesting your data
|
||||
</Text>
|
||||
{(sl > 0 || wiki > 0) && (
|
||||
<>
|
||||
<Text />
|
||||
<Text color={props.theme.text}>KLO created:</Text>
|
||||
<Text color={props.theme.text}>KTX created:</Text>
|
||||
{sl > 0 && (
|
||||
<Text color={props.theme.active}>
|
||||
{' '}📊 {sl} query definition{sl === 1 ? '' : 's'} — so agents can write accurate SQL for your data
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { EventEmitter } from 'node:events';
|
||||
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
|
||||
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { memoryFlowCommandForKey, renderMemoryFlowInteractively } from './memory-flow-interactive.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,26 +7,26 @@ import {
|
|||
type MemoryFlowInteractionCommand,
|
||||
type MemoryFlowInteractionState,
|
||||
type MemoryFlowReplayInput,
|
||||
} from '@klo/context/ingest';
|
||||
} from '@ktx/context/ingest';
|
||||
|
||||
interface KloMemoryFlowKey {
|
||||
interface KtxMemoryFlowKey {
|
||||
name?: string;
|
||||
ctrl?: boolean;
|
||||
}
|
||||
|
||||
export interface KloMemoryFlowStdin {
|
||||
export interface KtxMemoryFlowStdin {
|
||||
isTTY?: boolean;
|
||||
isRaw?: boolean;
|
||||
setRawMode?(value: boolean): void;
|
||||
resume?(): void;
|
||||
pause?(): void;
|
||||
on(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
|
||||
off?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
|
||||
removeListener?(event: 'keypress', listener: (chunk: string, key: KloMemoryFlowKey) => void): this;
|
||||
on(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
|
||||
off?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
|
||||
removeListener?(event: 'keypress', listener: (chunk: string, key: KtxMemoryFlowKey) => void): this;
|
||||
}
|
||||
|
||||
interface KloMemoryFlowInteractiveIo {
|
||||
stdin?: KloMemoryFlowStdin;
|
||||
interface KtxMemoryFlowInteractiveIo {
|
||||
stdin?: KtxMemoryFlowStdin;
|
||||
stdout: {
|
||||
isTTY?: boolean;
|
||||
columns?: number;
|
||||
|
|
@ -35,17 +35,17 @@ interface KloMemoryFlowInteractiveIo {
|
|||
}
|
||||
|
||||
interface RenderMemoryFlowInteractiveOptions {
|
||||
prepareKeypressEvents?(stdin: KloMemoryFlowStdin): void;
|
||||
prepareKeypressEvents?(stdin: KtxMemoryFlowStdin): void;
|
||||
}
|
||||
|
||||
function defaultPrepareKeypressEvents(stdin: KloMemoryFlowStdin): void {
|
||||
function defaultPrepareKeypressEvents(stdin: KtxMemoryFlowStdin): void {
|
||||
emitKeypressEvents(stdin as Parameters<typeof emitKeypressEvents>[0]);
|
||||
}
|
||||
|
||||
export function memoryFlowCommandForKey(
|
||||
chunk: string,
|
||||
search: MemoryFlowInteractionState['search'],
|
||||
key: KloMemoryFlowKey,
|
||||
key: KtxMemoryFlowKey,
|
||||
): MemoryFlowInteractionCommand | null {
|
||||
if (search.editing) {
|
||||
if (key.name === 'escape') return 'search-clear';
|
||||
|
|
@ -76,8 +76,8 @@ export function memoryFlowCommandForKey(
|
|||
}
|
||||
|
||||
function removeKeypressListener(
|
||||
stdin: KloMemoryFlowStdin,
|
||||
handler: (chunk: string, key: KloMemoryFlowKey) => void,
|
||||
stdin: KtxMemoryFlowStdin,
|
||||
handler: (chunk: string, key: KtxMemoryFlowKey) => void,
|
||||
): void {
|
||||
if (stdin.off) {
|
||||
stdin.off('keypress', handler);
|
||||
|
|
@ -86,7 +86,7 @@ function removeKeypressListener(
|
|||
stdin.removeListener?.('keypress', handler);
|
||||
}
|
||||
|
||||
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KloMemoryFlowInteractiveIo): void {
|
||||
function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState, io: KtxMemoryFlowInteractiveIo): void {
|
||||
const view = buildMemoryFlowViewModel(input);
|
||||
io.stdout.write('\u001b[2J\u001b[H');
|
||||
io.stdout.write(renderMemoryFlowInteractive(view, state, { terminalWidth: io.stdout.columns }));
|
||||
|
|
@ -94,7 +94,7 @@ function repaint(input: MemoryFlowReplayInput, state: MemoryFlowInteractionState
|
|||
|
||||
export async function renderMemoryFlowInteractively(
|
||||
input: MemoryFlowReplayInput,
|
||||
io: KloMemoryFlowInteractiveIo,
|
||||
io: KtxMemoryFlowInteractiveIo,
|
||||
options: RenderMemoryFlowInteractiveOptions = {},
|
||||
): Promise<void> {
|
||||
const stdin = io.stdin;
|
||||
|
|
@ -119,7 +119,7 @@ export async function renderMemoryFlowInteractively(
|
|||
stdin.pause?.();
|
||||
};
|
||||
|
||||
const handleKeypress = (_chunk: string, key: KloMemoryFlowKey): void => {
|
||||
const handleKeypress = (_chunk: string, key: KtxMemoryFlowKey): void => {
|
||||
const command = memoryFlowCommandForKey(_chunk, state.search, key);
|
||||
if (!command) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* @jsxImportSource react */
|
||||
import type { MemoryFlowReplayInput } from '@klo/context/ingest';
|
||||
import type { MemoryFlowReplayInput } from '@ktx/context/ingest';
|
||||
import { render as renderInkTest } from 'ink-testing-library';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
renderMemoryFlowTui,
|
||||
sanitizeMemoryFlowTuiError,
|
||||
startLiveMemoryFlowTui,
|
||||
type KloMemoryFlowTuiIo,
|
||||
type KtxMemoryFlowTuiIo,
|
||||
type MemoryFlowInkInstance,
|
||||
type MemoryFlowInkRenderOptions,
|
||||
} from './memory-flow-tui.js';
|
||||
|
|
@ -72,7 +72,7 @@ function packagedReplayInput(overrides: Partial<MemoryFlowReplayInput> = {}): Me
|
|||
};
|
||||
}
|
||||
|
||||
function makeIo(): { io: KloMemoryFlowTuiIo; stderr: () => string } {
|
||||
function makeIo(): { io: KtxMemoryFlowTuiIo; stderr: () => string } {
|
||||
let stderr = '';
|
||||
return { io: { stdin: { isTTY: true, setRawMode: vi.fn() }, stdout: { isTTY: true, columns: 120, write: vi.fn() }, stderr: { write(chunk: string) { stderr += chunk; } } }, stderr: () => stderr };
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ describe('sanitizeMemoryFlowTuiError', () => {
|
|||
});
|
||||
|
||||
describe('MemoryFlowTuiApp', () => {
|
||||
it('always shows the KLO logo', () => {
|
||||
it('always shows the KTX logo', () => {
|
||||
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={replayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
|
||||
expect(lastFrame()).toContain('█████╔╝');
|
||||
});
|
||||
|
|
@ -198,12 +198,12 @@ describe('MemoryFlowTuiApp', () => {
|
|||
expect(frame).toContain('Created so far:');
|
||||
expect(frame).toContain('order lifecycle');
|
||||
expect(frame).toContain('customer metrics');
|
||||
expect(frame).toContain('KLO finished ingesting your data');
|
||||
expect(frame).toContain('klo sl list');
|
||||
expect(frame).toContain('klo wiki list');
|
||||
expect(frame).toContain('klo serve --mcp stdio --user-id local');
|
||||
expect(frame).not.toContain(['klo', 'ask'].join(' '));
|
||||
expect(frame).not.toContain(['klo', 'mcp'].join(' '));
|
||||
expect(frame).toContain('KTX finished ingesting your data');
|
||||
expect(frame).toContain('ktx sl list');
|
||||
expect(frame).toContain('ktx wiki list');
|
||||
expect(frame).toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(frame).not.toContain(['ktx', 'ask'].join(' '));
|
||||
expect(frame).not.toContain(['ktx', 'mcp'].join(' '));
|
||||
});
|
||||
|
||||
it('handles quit while running', async () => {
|
||||
|
|
@ -254,7 +254,7 @@ describe('MemoryFlowTuiApp', () => {
|
|||
|
||||
it('hides completion while running', () => {
|
||||
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={runningReplayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
|
||||
expect(lastFrame()).not.toContain('KLO finished ingesting');
|
||||
expect(lastFrame()).not.toContain('KTX finished ingesting');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
reduceMemoryFlowInteractionState,
|
||||
selectedMemoryFlowColumn,
|
||||
selectedMemoryFlowDetails,
|
||||
} from '@klo/context/ingest';
|
||||
} from '@ktx/context/ingest';
|
||||
import { Box, Text, render as renderInkRuntime, useApp, useInput } from 'ink';
|
||||
import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { buildDemoMetrics } from './demo-metrics.js';
|
||||
|
|
@ -56,7 +56,7 @@ const STAGE_LABELS = {
|
|||
saved: 'MEMORY',
|
||||
} satisfies Record<MemoryFlowColumnId, string>;
|
||||
|
||||
export interface KloMemoryFlowTuiIo {
|
||||
export interface KtxMemoryFlowTuiIo {
|
||||
stdin?: { isTTY?: boolean; setRawMode?(value: boolean): void };
|
||||
stdout: { isTTY?: boolean; columns?: number; write(chunk: string): void };
|
||||
stderr: { write(chunk: string): void };
|
||||
|
|
@ -76,9 +76,9 @@ export interface MemoryFlowInkInstance {
|
|||
}
|
||||
|
||||
export interface MemoryFlowInkRenderOptions {
|
||||
stdin?: KloMemoryFlowTuiIo['stdin'];
|
||||
stdout: KloMemoryFlowTuiIo['stdout'];
|
||||
stderr: KloMemoryFlowTuiIo['stderr'];
|
||||
stdin?: KtxMemoryFlowTuiIo['stdin'];
|
||||
stdout: KtxMemoryFlowTuiIo['stdout'];
|
||||
stderr: KtxMemoryFlowTuiIo['stderr'];
|
||||
exitOnCtrlC: boolean;
|
||||
patchConsole: boolean;
|
||||
maxFps: number;
|
||||
|
|
@ -429,7 +429,7 @@ export function MemoryFlowTuiApp(props: MemoryFlowTuiAppProps): ReactNode {
|
|||
|
||||
function renderTree(
|
||||
input: MemoryFlowReplayInput,
|
||||
io: KloMemoryFlowTuiIo,
|
||||
io: KtxMemoryFlowTuiIo,
|
||||
onExit: () => void,
|
||||
options: RenderTreeOptions = {},
|
||||
): ReactNode {
|
||||
|
|
@ -459,7 +459,7 @@ function renderInk(tree: ReactNode, options: MemoryFlowInkRenderOptions): Memory
|
|||
}) as MemoryFlowInkInstance;
|
||||
}
|
||||
|
||||
function renderOptions(io: KloMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
|
||||
function renderOptions(io: KtxMemoryFlowTuiIo): MemoryFlowInkRenderOptions {
|
||||
return {
|
||||
stdin: io.stdin,
|
||||
stdout: io.stdout,
|
||||
|
|
@ -491,7 +491,7 @@ function resolveTiming(options: RenderMemoryFlowTuiOptions): MemoryFlowTuiTiming
|
|||
|
||||
export async function renderMemoryFlowTui(
|
||||
input: MemoryFlowReplayInput,
|
||||
io: KloMemoryFlowTuiIo,
|
||||
io: KtxMemoryFlowTuiIo,
|
||||
options: RenderMemoryFlowTuiOptions = {},
|
||||
): Promise<boolean> {
|
||||
let instance: MemoryFlowInkInstance | null = null;
|
||||
|
|
@ -516,7 +516,7 @@ export async function renderMemoryFlowTui(
|
|||
|
||||
export async function startLiveMemoryFlowTui(
|
||||
input: MemoryFlowReplayInput,
|
||||
io: KloMemoryFlowTuiIo,
|
||||
io: KtxMemoryFlowTuiIo,
|
||||
options: StartLiveMemoryFlowTuiOptions = {},
|
||||
): Promise<MemoryFlowTuiLiveSession | null> {
|
||||
let instance: MemoryFlowInkInstance | null = null;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue