refactor(cli): hide internal setup options and remove dead flags

Hide advanced/internal `ktx setup` options from --help output using
.hideHelp() so the command surface is approachable for new users. Remove
the --project, --agent-scope, and --skip-initial-source-ingest flags
that are no longer needed. Update docs and tests to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luca Martial 2026-05-13 14:53:41 -07:00
parent f219ba22a6
commit a9bc0431ca
5 changed files with 185 additions and 198 deletions

View file

@ -18,8 +18,6 @@ ktx setup [options]
| Flag | Description | Default |
|------|-------------|---------|
| `--project-dir <path>` | KTX project directory | `KTX_PROJECT_DIR`, nearest `ktx.yaml`, or cwd |
| `--new` | Create a new KTX project before setup | `false` |
| `--existing` | Use an existing KTX project | `false` |
| `--yes` | Accept safe defaults in non-interactive setup | `false` |
| `--no-input` | Disable interactive terminal input | — |
@ -29,76 +27,11 @@ ktx setup [options]
|------|-------------|---------|
| `--agents` | Install agent integration only | `false` |
| `--target <target>` | Agent target (`claude-code`, `codex`, `cursor`, `opencode`, `universal`) | — |
| `--agent-scope <scope>` | Agent install scope (`project` or `global`) | `project` |
| `--project` | Install agent integration into the project scope | `false` |
| `--global` | Install agent integration into the global target scope (Claude Code and Codex only) | `false` |
| `--skip-agents` | Leave agent integration incomplete for now | `false` |
### LLM Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--anthropic-api-key-env <name>` | Environment variable containing the Anthropic API key | — |
| `--anthropic-api-key-file <path>` | File containing the Anthropic API key | — |
| `--anthropic-model <model>` | Anthropic model ID to validate and save | — |
| `--skip-llm` | Leave LLM setup incomplete for now | `false` |
### Embedding Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--embedding-backend <backend>` | Embedding backend (`openai` or `sentence-transformers`) | — |
| `--embedding-api-key-env <name>` | Environment variable containing the embedding provider API key | — |
| `--embedding-api-key-file <path>` | File containing the embedding provider API key | — |
| `--skip-embeddings` | Leave embedding setup incomplete for now | `false` |
### Database Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--database <driver>` | Database driver to configure; repeatable (`sqlite`, `postgres`, `mysql`, `clickhouse`, `sqlserver`, `bigquery`, `snowflake`) | — |
| `--database-connection-id <id>` | Existing or new connection id; repeatable | — |
| `--new-database-connection-id <id>` | Connection id for one new database connection | — |
| `--database-url <url>` | URL, `env:NAME`, or `file:/path` for one new URL-style database connection | — |
| `--database-schema <schema>` | Database schema to include; repeatable | — |
| `--skip-databases` | Leave database setup incomplete | `false` |
### Historic SQL
| Flag | Description | Default |
|------|-------------|---------|
| `--enable-historic-sql` | Enable Historic SQL when the selected database supports it | `false` |
| `--disable-historic-sql` | Disable Historic SQL for the selected database | `false` |
| `--historic-sql-window-days <number>` | Historic SQL query-history window in days | — |
| `--historic-sql-min-executions <number>` | Minimum executions for a Historic SQL template | — |
| `--historic-sql-min-calls <number>` | Alias for `--historic-sql-min-executions` for one release | — |
| `--historic-sql-service-account-pattern <pattern>` | Historic SQL service-account regex; repeatable | — |
| `--historic-sql-redaction-pattern <pattern>` | Historic SQL SQL-literal redaction regex; repeatable | — |
### Context Source Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--source <type>` | Source connector type (`dbt`, `metricflow`, `metabase`, `looker`, `lookml`, `notion`) | — |
| `--source-connection-id <id>` | Connection id for source setup | — |
| `--source-path <path>` | Local source path for dbt, MetricFlow, or LookML | — |
| `--source-git-url <url>` | Git URL for dbt, MetricFlow, or LookML | — |
| `--source-branch <branch>` | Git branch for source setup | — |
| `--source-subpath <path>` | Repo subpath for source setup | — |
| `--source-auth-token-ref <ref>` | `env:` or `file:` credential ref for source repo auth | — |
| `--source-url <url>` | Source service URL for Metabase or Looker | — |
| `--source-api-key-ref <ref>` | `env:` or `file:` API key ref for Metabase or Notion | — |
| `--source-client-id <id>` | Looker client id | — |
| `--source-client-secret-ref <ref>` | `env:` or `file:` Looker client secret ref | — |
| `--source-warehouse-connection-id <id>` | Mapped warehouse connection id | — |
| `--source-project-name <name>` | dbt project name override | — |
| `--source-profiles-path <path>` | dbt profiles path | — |
| `--source-target <target>` | dbt target or source-specific mapping target | — |
| `--metabase-database-id <id>` | Metabase database id to map | — |
| `--notion-crawl-mode <mode>` | Notion crawl mode (`all_accessible` or `selected_roots`) | — |
| `--notion-root-page-id <id>` | Notion root page id; repeatable | — |
| `--skip-initial-source-ingest` | Validate source setup without building source context during setup | `false` |
| `--skip-sources` | Mark optional source setup complete with no sources | `false` |
The setup wizard is the public configuration interface. It prompts for LLM
credentials, embeddings, database connections, context sources, Historic SQL,
and agent integration when those values are needed.
## Examples
@ -106,17 +39,8 @@ ktx setup [options]
# Run the interactive setup wizard
ktx setup
# Create a new project and run setup
ktx setup --new
# Resume setup in an existing project
ktx setup --existing
# Non-interactive setup with Anthropic key from environment
ktx setup --yes --anthropic-api-key-env ANTHROPIC_API_KEY
# Set up a Postgres connection
ktx setup --database postgres --database-url "env:DATABASE_URL"
# Run setup for a specific project directory
ktx setup --project-dir ./analytics
# Install agent integration for Claude Code only
ktx setup --agents --target claude-code
@ -124,12 +48,6 @@ ktx setup --agents --target claude-code
# Install agent integration globally for Codex
ktx setup --agents --target codex --global
# Add a dbt source from a local path
ktx setup --source dbt --source-path ./my-dbt-project
# Skip optional steps for a minimal setup
ktx setup --skip-sources --skip-agents
# Check setup readiness
ktx status
```
@ -156,5 +74,5 @@ Agent integration ready: yes (codex:project)
|-------|-------|----------|
| Setup resumes an unexpected project | `KTX_PROJECT_DIR` or nearest `ktx.yaml` points to another directory | Pass `--project-dir <path>` explicitly |
| Health check for model fails | Provider key or model id is invalid | Set the correct environment variable or secret file and rerun setup |
| Setup cannot run in CI | Interactive prompts need a TTY | Use `--yes --no-input` with explicit flags for required values |
| Setup cannot run in CI | Interactive prompts need a TTY | Run setup interactively before CI, or provide a fixture `ktx.yaml` for automated tests |
| Agent integration missing | Setup skipped the agents step | Run `ktx setup --agents --target <target>` |

View file

@ -242,7 +242,7 @@ Agent integration ready: yes (claude-code:project)
| Local embeddings hang or fail | The managed Python runtime cannot start or the local model runtime is unavailable | Install `uv`, run `ktx dev runtime status`, then run `ktx dev runtime install --feature local-embeddings --yes` and rerun setup |
| Database connection test fails | Credentials, network access, warehouse, database, or schema value is wrong | Test the same URL with the database's native client, then rerun `ktx setup` and reconfigure the connection |
| `KTX context built: no` in `ktx status` | Setup saved configuration but did not build context | Run `ktx setup` and choose to build context now |
| Agent integration is incomplete | Setup skipped the agents step or the target was not installed | Run `ktx setup --agents --target codex --project` using the target you need |
| Agent integration is incomplete | Setup skipped the agents step or the target was not installed | Run `ktx setup --agents --target codex` using the target you need |
## Next steps

View file

@ -64,13 +64,6 @@ function sourceType(value: string): KtxSetupSourceType {
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function agentScope(value: string): 'project' | 'global' {
if (value === 'project' || value === 'global') {
return value;
}
throw new InvalidArgumentError(`invalid choice '${value}'`);
}
function positiveNumber(value: string): number {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
@ -97,7 +90,6 @@ function shouldShowSetupEntryMenu(
agents?: boolean;
target?: string;
global?: boolean;
project?: boolean;
skipAgents?: boolean;
yes?: boolean;
input?: boolean;
@ -142,7 +134,6 @@ function shouldShowSetupEntryMenu(
metabaseDatabaseId?: number;
notionCrawlMode?: string;
notionRootPageId?: string[];
skipInitialSourceIngest?: boolean;
skipSources?: boolean;
},
command: Command,
@ -172,7 +163,6 @@ function shouldShowSetupEntryMenu(
'agents',
'target',
'global',
'project',
'skipAgents',
'yes',
'input',
@ -211,7 +201,6 @@ function shouldShowSetupEntryMenu(
'sourceTarget',
'metabaseDatabaseId',
'notionCrawlMode',
'skipInitialSourceIngest',
'skipSources',
].some((optionName) => optionWasSpecified(command, optionName));
}
@ -220,9 +209,9 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
const setup = program
.command('setup')
.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)
.addOption(new Option('--project-dir <path>', 'KTX project directory').hideHelp())
.addOption(new Option('--new', 'Create a new KTX project before setup').hideHelp().default(false))
.addOption(new Option('--existing', 'Use an existing KTX project').hideHelp().default(false))
.option('--agents', 'Install agent integration only', false)
.addOption(
new Option('--target <target>', 'Agent target').choices([
@ -233,94 +222,124 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
'universal',
]),
)
.addOption(new Option('--agent-scope <scope>', 'Agent install scope').argParser(agentScope).default('project'))
.option('--project', 'Install agent integration into the project scope', false)
.option('--global', 'Install agent integration into the global target scope', false)
.option('--skip-agents', 'Leave agent integration incomplete for now', false)
.addOption(new Option('--skip-agents', 'Leave agent integration incomplete for now').hideHelp().default(false))
.option('--yes', 'Accept safe defaults in non-interactive setup', false)
.option('--no-input', 'Disable interactive terminal input')
.addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend))
.option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key')
.option('--anthropic-api-key-file <path>', 'File containing the Anthropic API key')
.option('--anthropic-model <model>', 'Anthropic model ID to validate and save')
.option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path')
.option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path')
.addOption(new Option('--skip-llm', 'Leave LLM setup incomplete for now').hideHelp().default(false))
.addOption(new Option('--embedding-backend <backend>', 'Embedding backend').argParser(embeddingBackend))
.option('--embedding-api-key-env <name>', 'Environment variable containing the embedding provider API key')
.option('--embedding-api-key-file <path>', 'File containing the embedding provider API key')
.addOption(new Option('--skip-embeddings', 'Leave embedding setup incomplete for now').hideHelp().default(false))
.option(
'--database <driver>',
'Database driver to configure; repeatable',
(value, previous: KtxSetupDatabaseDriver[]) => {
return [...previous, databaseDriver(value)];
},
[] as KtxSetupDatabaseDriver[],
)
.option(
'--database-connection-id <id>',
'Existing selected connection id or new connection id',
(value, previous: string[]) => [...previous, value],
[],
)
.option('--new-database-connection-id <id>', 'Connection id for one new database connection', (value) => {
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
throw new InvalidArgumentError(`Unsafe connection id: ${value}`);
}
return value;
})
.option('--database-url <url>', 'URL, env:NAME, or file:/path for one new URL-style database connection')
.option(
'--database-schema <schema>',
'Database schema to include; repeatable',
(value, previous: string[]) => [...previous, value],
[],
)
.option('--enable-historic-sql', 'Enable Historic SQL when the selected database supports it', false)
.option('--disable-historic-sql', 'Disable Historic SQL for the selected database', false)
.option('--historic-sql-window-days <number>', 'Historic SQL query-history window', positiveInteger)
.option('--historic-sql-min-executions <number>', 'Minimum Historic SQL executions for a template', positiveInteger)
.option(
'--historic-sql-service-account-pattern <pattern>',
'Historic SQL service-account regex; repeatable',
(value, previous: string[]) => [...previous, value],
[],
)
.option(
'--historic-sql-redaction-pattern <pattern>',
'Historic SQL SQL-literal redaction regex; repeatable',
(value, previous: string[]) => [...previous, value],
[],
)
.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')
.option('--source-git-url <url>', 'Git URL for dbt, MetricFlow, or LookML')
.option('--source-branch <branch>', 'Git branch for source setup')
.option('--source-subpath <path>', 'Repo subpath for source setup')
.option('--source-auth-token-ref <ref>', 'env: or file: credential ref for source repo auth')
.option('--source-url <url>', 'Source service URL for Metabase or Looker')
.option('--source-api-key-ref <ref>', 'env: or file: API key ref for Metabase or Notion')
.option('--source-client-id <id>', 'Looker client id')
.option('--source-client-secret-ref <ref>', 'env: or file: Looker client secret ref')
.option('--source-warehouse-connection-id <id>', 'Mapped warehouse connection id')
.option('--source-project-name <name>', 'dbt project name override')
.option('--source-profiles-path <path>', 'dbt profiles path')
.option('--source-target <target>', 'dbt target or source-specific mapping target')
.option('--metabase-database-id <id>', 'Metabase database id to map', positiveNumber)
.addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend).hideHelp())
.addOption(
new Option('--notion-crawl-mode <mode>', 'Notion crawl mode').choices(['all_accessible', 'selected_roots']),
new Option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key').hideHelp(),
)
.option(
'--notion-root-page-id <id>',
'Notion root page id; repeatable',
(value, previous: string[]) => [...previous, value],
[],
.addOption(
new Option('--anthropic-api-key-file <path>', 'File containing the Anthropic API key').hideHelp(),
)
.option('--skip-initial-source-ingest', 'Validate source setup without building source context during setup', false)
.option('--skip-sources', 'Mark optional source setup complete with no sources', false)
.addOption(new Option('--anthropic-model <model>', 'Anthropic model ID to validate and save').hideHelp())
.addOption(new Option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp())
.addOption(new Option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path').hideHelp())
.addOption(new Option('--skip-llm', 'Leave LLM setup incomplete for now').hideHelp().default(false))
.addOption(new Option('--embedding-backend <backend>', 'Embedding backend').argParser(embeddingBackend).hideHelp())
.addOption(
new Option(
'--embedding-api-key-env <name>',
'Environment variable containing the embedding provider API key',
).hideHelp(),
)
.addOption(
new Option('--embedding-api-key-file <path>', 'File containing the embedding provider API key').hideHelp(),
)
.addOption(new Option('--skip-embeddings', 'Leave embedding setup incomplete for now').hideHelp().default(false))
.addOption(
new Option('--database <driver>', 'Database driver to configure; repeatable')
.argParser((value, previous: KtxSetupDatabaseDriver[]) => {
return [...previous, databaseDriver(value)];
})
.default([] as KtxSetupDatabaseDriver[])
.hideHelp(),
)
.addOption(
new Option('--database-connection-id <id>', 'Existing selected connection id or new connection id')
.argParser((value, previous: string[]) => [...previous, value])
.default([] as string[])
.hideHelp(),
)
.addOption(
new Option('--new-database-connection-id <id>', 'Connection id for one new database connection')
.argParser((value) => {
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
throw new InvalidArgumentError(`Unsafe connection id: ${value}`);
}
return value;
})
.hideHelp(),
)
.addOption(
new Option('--database-url <url>', 'URL, env:NAME, or file:/path for one new URL-style database connection').hideHelp(),
)
.addOption(
new Option('--database-schema <schema>', 'Database schema to include; repeatable')
.argParser((value, previous: string[]) => [...previous, value])
.default([] as string[])
.hideHelp(),
)
.addOption(
new Option('--enable-historic-sql', 'Enable Historic SQL when the selected database supports it')
.hideHelp()
.default(false),
)
.addOption(
new Option('--disable-historic-sql', 'Disable Historic SQL for the selected database').hideHelp().default(false),
)
.addOption(new Option('--historic-sql-window-days <number>', 'Historic SQL query-history window').argParser(positiveInteger).hideHelp())
.addOption(
new Option('--historic-sql-min-executions <number>', 'Minimum Historic SQL executions for a template')
.argParser(positiveInteger)
.hideHelp(),
)
.addOption(
new Option('--historic-sql-service-account-pattern <pattern>', 'Historic SQL service-account regex; repeatable')
.argParser((value, previous: string[]) => [...previous, value])
.default([] as string[])
.hideHelp(),
)
.addOption(
new Option('--historic-sql-redaction-pattern <pattern>', 'Historic SQL SQL-literal redaction regex; repeatable')
.argParser((value, previous: string[]) => [...previous, value])
.default([] as string[])
.hideHelp(),
)
.addOption(
new Option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a primary source is added')
.hideHelp()
.default(false),
)
.addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType).hideHelp())
.addOption(new Option('--source-connection-id <id>', 'Connection id for source setup').hideHelp())
.addOption(new Option('--source-path <path>', 'Local source path for dbt, MetricFlow, or LookML').hideHelp())
.addOption(new Option('--source-git-url <url>', 'Git URL for dbt, MetricFlow, or LookML').hideHelp())
.addOption(new Option('--source-branch <branch>', 'Git branch for source setup').hideHelp())
.addOption(new Option('--source-subpath <path>', 'Repo subpath for source setup').hideHelp())
.addOption(new Option('--source-auth-token-ref <ref>', 'env: or file: credential ref for source repo auth').hideHelp())
.addOption(new Option('--source-url <url>', 'Source service URL for Metabase or Looker').hideHelp())
.addOption(new Option('--source-api-key-ref <ref>', 'env: or file: API key ref for Metabase or Notion').hideHelp())
.addOption(new Option('--source-client-id <id>', 'Looker client id').hideHelp())
.addOption(new Option('--source-client-secret-ref <ref>', 'env: or file: Looker client secret ref').hideHelp())
.addOption(new Option('--source-warehouse-connection-id <id>', 'Mapped warehouse connection id').hideHelp())
.addOption(new Option('--source-project-name <name>', 'dbt project name override').hideHelp())
.addOption(new Option('--source-profiles-path <path>', 'dbt profiles path').hideHelp())
.addOption(new Option('--source-target <target>', 'dbt target or source-specific mapping target').hideHelp())
.addOption(new Option('--metabase-database-id <id>', 'Metabase database id to map').argParser(positiveNumber).hideHelp())
.addOption(
new Option('--notion-crawl-mode <mode>', 'Notion crawl mode')
.choices(['all_accessible', 'selected_roots'])
.hideHelp(),
)
.addOption(
new Option('--notion-root-page-id <id>', 'Notion root page id; repeatable')
.argParser((value, previous: string[]) => [...previous, value])
.default([] as string[])
.hideHelp(),
)
.addOption(new Option('--skip-sources', 'Mark optional source setup complete with no sources').hideHelp().default(false))
.showHelpAfterError();
setup.hook('preAction', (_thisCommand, actionCommand) => {
@ -371,7 +390,7 @@ export function registerSetupCommands(program: Command, context: KtxCliCommandCo
}
const mode = options.new ? 'new' : options.existing ? 'existing' : 'auto';
const resolvedAgentScope = options.global ? 'global' : options.agentScope;
const resolvedAgentScope = options.global ? 'global' : 'project';
await runSetupArgs(context, {
command: 'run',
projectDir: resolveCommandProjectDir(command),

View file

@ -444,20 +444,54 @@ describe('runKtxCli', () => {
expect(io.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
});
it('documents setup as a bare command without subcommands', async () => {
it('documents setup with only the common interactive options visible', async () => {
const testIo = makeIo();
await expect(runKtxCli(['setup', '--help'], testIo.io)).resolves.toBe(0);
expect(testIo.stdout()).toContain('Usage: ktx setup [options]');
expect(testIo.stdout()).not.toContain('Commands:');
expect(testIo.stdout()).not.toContain('setup demo');
expect(testIo.stdout()).not.toContain('setup context');
expect(testIo.stdout()).not.toContain('--skip-llm');
expect(testIo.stdout()).not.toContain('--skip-embeddings');
expect(testIo.stdout()).not.toContain('--embedding-model');
expect(testIo.stdout()).not.toContain('--embedding-dimensions');
expect(testIo.stdout()).not.toContain('--embedding-base-url');
const stdout = testIo.stdout();
expect(stdout).toContain('Usage: ktx setup [options]');
expect(stdout).toContain('--agents');
expect(stdout).toContain('--target <target>');
expect(stdout).toContain('--global');
expect(stdout).toContain('--yes');
expect(stdout).toContain('--no-input');
expect(stdout).toContain('Global Options:');
expect(stdout.match(/--project-dir <path>/g)).toHaveLength(1);
expect(stdout).not.toContain('Commands:');
expect(stdout).not.toContain('setup demo');
expect(stdout).not.toContain('setup context');
for (const hiddenFlag of [
'--new',
'--existing',
'--agent-scope',
'--skip-agents',
'--llm-backend',
'--anthropic-api-key-env',
'--vertex-project',
'--embedding-backend',
'--database ',
'--database-connection-id',
'--new-database-connection-id',
'--enable-historic-sql',
'--historic-sql-min-executions',
'--skip-databases',
'--source ',
'--source-connection-id',
'--metabase-database-id',
'--notion-root-page-id',
'--skip-initial-source-ingest',
'--skip-sources',
'--skip-llm',
'--skip-embeddings',
'--embedding-model',
'--embedding-dimensions',
'--embedding-base-url',
]) {
expect(stdout).not.toContain(hiddenFlag);
}
expect(stdout).not.toMatch(/^ --project\s/m);
expect(testIo.stderr()).toBe('');
});
@ -725,6 +759,23 @@ describe('runKtxCli', () => {
expect(setup).not.toHaveBeenCalled();
});
it('rejects removed setup options', async () => {
const setup = vi.fn(async () => 0);
const cases = [
['setup', '--project'],
['setup', '--agent-scope', 'global'],
['setup', '--skip-initial-source-ingest'],
];
for (const args of cases) {
const testIo = makeIo();
await expect(runKtxCli(['--project-dir', tempDir, ...args], testIo.io, { setup })).resolves.toBe(1);
expect(testIo.stderr()).toMatch(/unknown option|error:/i);
}
expect(setup).not.toHaveBeenCalled();
});
it('prints ingest help without invoking ingest execution', async () => {
const testIo = makeIo();
const ingest = vi.fn();
@ -1250,7 +1301,6 @@ describe('runKtxCli', () => {
'--agents',
'--target',
'codex',
'--project',
'--no-input',
'--yes',
],

View file

@ -82,7 +82,7 @@ export function plannedKtxAgentFiles(input: {
{ kind: 'file', path: join(codexHome, 'instructions/ktx.md'), role: 'rule' as const },
];
}
throw new Error(`Global ${input.target} installation is not supported; use --project.`);
throw new Error(`Global ${input.target} installation is not supported; omit --global.`);
}
const root = resolve(input.projectDir);