@kaelio/ktx 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/python/{kaelio_ktx-0.1.0-py3-none-any.whl → kaelio_ktx-0.2.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/admin-reindex.d.ts +15 -0
- package/dist/admin-reindex.js +168 -0
- package/dist/admin-reindex.test.js +116 -0
- package/dist/{dev.d.ts → admin.d.ts} +1 -1
- package/dist/{dev.js → admin.js} +14 -12
- package/dist/admin.test.d.ts +1 -0
- package/dist/{dev.test.js → admin.test.js} +36 -31
- package/dist/cli-program.js +7 -7
- package/dist/cli-program.test.js +1 -1
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/commands/connection-commands.js +11 -10
- package/dist/commands/connection-selection.d.ts +11 -0
- package/dist/commands/connection-selection.js +9 -0
- package/dist/commands/ingest-commands.js +32 -26
- package/dist/commands/knowledge-commands.js +17 -28
- package/dist/commands/mcp-commands.js +17 -11
- package/dist/commands/setup-commands.js +14 -26
- package/dist/commands/sl-commands.js +27 -32
- package/dist/doctor.test.js +7 -8
- package/dist/example-smoke.test.js +3 -3
- package/dist/index.test.js +102 -70
- package/dist/ingest-depth.js +0 -1
- package/dist/ingest.test-utils.js +2 -2
- package/dist/ingest.test.js +4 -4
- package/dist/io/print-list.test.js +4 -4
- package/dist/knowledge.js +1 -1
- package/dist/managed-local-embeddings.d.ts +2 -0
- package/dist/managed-local-embeddings.js +2 -0
- package/dist/managed-local-embeddings.test.js +2 -0
- package/dist/managed-mcp-daemon.js +3 -2
- package/dist/managed-mcp-daemon.test.js +25 -0
- package/dist/managed-python-command.js +2 -2
- package/dist/managed-python-command.test.js +4 -3
- package/dist/managed-python-daemon.js +3 -2
- package/dist/managed-python-daemon.test.js +20 -0
- package/dist/managed-python-runtime.d.ts +5 -1
- package/dist/managed-python-runtime.js +50 -6
- package/dist/managed-python-runtime.test.js +53 -23
- package/dist/memory-flow-tui.test.js +2 -2
- package/dist/next-steps.d.ts +6 -6
- package/dist/next-steps.js +4 -4
- package/dist/next-steps.test.js +5 -5
- package/dist/print-command-tree.test.js +1 -1
- package/dist/proxy-env.d.ts +1 -0
- package/dist/proxy-env.js +23 -0
- package/dist/proxy-env.test.d.ts +1 -0
- package/dist/proxy-env.test.js +17 -0
- package/dist/public-ingest.js +3 -5
- package/dist/public-ingest.test.js +7 -3
- package/dist/runtime.test.js +2 -1
- package/dist/scan.test.js +2 -2
- package/dist/setup-agents.js +6 -4
- package/dist/setup-agents.test.js +35 -1
- package/dist/setup-embeddings.d.ts +1 -0
- package/dist/setup-embeddings.js +29 -7
- package/dist/setup-embeddings.test.js +49 -7
- package/dist/setup-models.d.ts +0 -1
- package/dist/setup-models.js +2 -3
- package/dist/setup-models.test.js +8 -10
- package/dist/setup-project.d.ts +9 -1
- package/dist/setup-project.js +52 -25
- package/dist/setup-project.test.js +8 -8
- package/dist/setup-runtime.test.js +4 -2
- package/dist/setup.d.ts +1 -2
- package/dist/setup.js +21 -5
- package/dist/setup.test.js +160 -43
- package/dist/sl.js +1 -1
- package/dist/sl.test.js +2 -1
- package/dist/standalone-smoke.test.js +8 -5
- package/dist/status-project.js +1 -10
- package/node_modules/@ktx/context/dist/index-sync/index.d.ts +2 -0
- package/node_modules/@ktx/context/dist/index-sync/index.js +1 -0
- package/node_modules/@ktx/context/dist/index-sync/reindex.d.ts +20 -0
- package/node_modules/@ktx/context/dist/index-sync/reindex.js +141 -0
- package/node_modules/@ktx/context/dist/index-sync/reindex.test.d.ts +1 -0
- package/node_modules/@ktx/context/dist/index-sync/reindex.test.js +139 -0
- package/node_modules/@ktx/context/dist/index-sync/types.d.ts +29 -0
- package/node_modules/@ktx/context/dist/index-sync/types.js +1 -0
- package/node_modules/@ktx/context/dist/index.d.ts +1 -0
- package/node_modules/@ktx/context/dist/index.js +1 -0
- package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +1 -1
- package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +8 -8
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -1
- package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
- package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
- package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
- package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -2
- package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
- package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
- package/node_modules/@ktx/context/dist/memory/local-memory.js +9 -3
- package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
- package/node_modules/@ktx/context/dist/project/config.js +5 -5
- package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
- package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
- package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
- package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
- package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
- package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
- package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
- package/node_modules/@ktx/context/dist/sl/ports.d.ts +3 -3
- package/node_modules/@ktx/context/dist/sl/sl-search.service.d.ts +3 -2
- package/node_modules/@ktx/context/dist/sl/sl-search.service.js +47 -45
- package/node_modules/@ktx/context/dist/sl/sl-search.service.test.js +61 -0
- package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.d.ts +4 -3
- package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.js +15 -5
- package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.test.js +24 -0
- package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.d.ts +3 -2
- package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.js +62 -51
- package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.test.js +59 -3
- package/node_modules/@ktx/context/dist/wiki/ports.d.ts +3 -3
- package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.d.ts +33 -0
- package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.js +155 -2
- package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.test.js +26 -0
- package/node_modules/@ktx/context/package.json +5 -0
- package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
- package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
- package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
- package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
- package/package.json +1 -1
- /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
package/dist/scan.test.js
CHANGED
|
@@ -287,8 +287,8 @@ describe('runKtxScan', () => {
|
|
|
287
287
|
expect(io.stdout()).toContain('Report: raw-sources/warehouse/live-database/sync-1/scan-report.json');
|
|
288
288
|
expect(io.stdout()).toContain('Next:\n');
|
|
289
289
|
expect(io.stdout()).toContain('ktx status --project-dir ');
|
|
290
|
-
expect(io.stdout()).not.toContain('ktx
|
|
291
|
-
expect(io.stdout()).not.toContain('ktx
|
|
290
|
+
expect(io.stdout()).not.toContain('ktx admin scan status');
|
|
291
|
+
expect(io.stdout()).not.toContain('ktx admin scan report');
|
|
292
292
|
expect(io.stdout()).not.toContain('\u001b[');
|
|
293
293
|
expect(io.stdout()).not.toContain('✓');
|
|
294
294
|
expect(io.stdout()).not.toContain('+1');
|
package/dist/setup-agents.js
CHANGED
|
@@ -426,8 +426,8 @@ function cliInstructionContent(input) {
|
|
|
426
426
|
'Available commands:',
|
|
427
427
|
'',
|
|
428
428
|
`- \`${ktxCommandLine(input.launcher, ['status', ...jsonProjectDirArgs])}\``,
|
|
429
|
-
`- \`${ktxCommandLine(input.launcher, ['sl',
|
|
430
|
-
`- \`${ktxCommandLine(input.launcher, ['sl', '
|
|
429
|
+
`- \`${ktxCommandLine(input.launcher, ['sl', ...jsonProjectDirArgs])}\``,
|
|
430
|
+
`- \`${ktxCommandLine(input.launcher, ['sl', '<text>', ...jsonProjectDirArgs, '--connection-id', '<id>'])}\``,
|
|
431
431
|
`- \`${ktxCommandLine(input.launcher, [
|
|
432
432
|
'sl',
|
|
433
433
|
'query',
|
|
@@ -442,7 +442,7 @@ function cliInstructionContent(input) {
|
|
|
442
442
|
'--max-rows',
|
|
443
443
|
'100',
|
|
444
444
|
])}\``,
|
|
445
|
-
`- \`${ktxCommandLine(input.launcher, ['wiki', '
|
|
445
|
+
`- \`${ktxCommandLine(input.launcher, ['wiki', '<query>', ...jsonProjectDirArgs, '--limit', '10'])}\``,
|
|
446
446
|
'',
|
|
447
447
|
'Use semantic-layer queries before direct database access. Do not print secrets or credential references.',
|
|
448
448
|
'',
|
|
@@ -965,7 +965,9 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
|
|
|
965
965
|
if (targets.includes('back'))
|
|
966
966
|
return { status: 'back', projectDir: args.projectDir };
|
|
967
967
|
if (targets.length === 0) {
|
|
968
|
-
io.stderr.write(
|
|
968
|
+
io.stderr.write(args.inputMode === 'disabled'
|
|
969
|
+
? 'Run in a TTY, or pass --target <target>.\n'
|
|
970
|
+
: 'Missing agent target: pass --target or use interactive setup.\n');
|
|
969
971
|
return { status: 'missing-input', projectDir: args.projectDir };
|
|
970
972
|
}
|
|
971
973
|
const scopeTargets = targets.filter((target) => target !== 'claude-desktop');
|
|
@@ -149,7 +149,7 @@ describe('setup agents', () => {
|
|
|
149
149
|
expect(skill).toContain(`--project-dir ${tempDir}`);
|
|
150
150
|
expect(skill).toContain('must not print secrets');
|
|
151
151
|
expect(skill).toContain('status --json');
|
|
152
|
-
expect(skill).toContain('sl
|
|
152
|
+
expect(skill).toContain('sl --json');
|
|
153
153
|
expect(skill).toContain('sl query');
|
|
154
154
|
expect(skill).toContain('--format json');
|
|
155
155
|
expect(skill).not.toContain('sl query --json');
|
|
@@ -163,6 +163,40 @@ describe('setup agents', () => {
|
|
|
163
163
|
expect(await readKtxSetupState(tempDir)).toEqual({ completed_steps: ['agents'] });
|
|
164
164
|
expect(io.stderr()).toBe('');
|
|
165
165
|
});
|
|
166
|
+
it('installs a specified target in non-interactive mode without --yes', async () => {
|
|
167
|
+
const io = makeIo();
|
|
168
|
+
await expect(runKtxSetupAgentsStep({
|
|
169
|
+
projectDir: tempDir,
|
|
170
|
+
inputMode: 'disabled',
|
|
171
|
+
yes: false,
|
|
172
|
+
agents: true,
|
|
173
|
+
target: 'claude-code',
|
|
174
|
+
scope: 'project',
|
|
175
|
+
mode: 'mcp',
|
|
176
|
+
skipAgents: false,
|
|
177
|
+
}, io.io)).resolves.toMatchObject({
|
|
178
|
+
status: 'ready',
|
|
179
|
+
projectDir: tempDir,
|
|
180
|
+
installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp' }],
|
|
181
|
+
});
|
|
182
|
+
await expect(stat(join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
|
|
183
|
+
const mcpConfig = JSON.parse(await readFile(join(tempDir, '.mcp.json'), 'utf-8'));
|
|
184
|
+
expect(mcpConfig.mcpServers).toHaveProperty('ktx');
|
|
185
|
+
expect(io.stderr()).toBe('');
|
|
186
|
+
});
|
|
187
|
+
it('prints concrete target guidance when non-interactive agent setup has no target', async () => {
|
|
188
|
+
const io = makeIo();
|
|
189
|
+
await expect(runKtxSetupAgentsStep({
|
|
190
|
+
projectDir: tempDir,
|
|
191
|
+
inputMode: 'disabled',
|
|
192
|
+
yes: false,
|
|
193
|
+
agents: true,
|
|
194
|
+
scope: 'project',
|
|
195
|
+
mode: 'mcp',
|
|
196
|
+
skipAgents: false,
|
|
197
|
+
}, io.io)).resolves.toEqual({ status: 'missing-input', projectDir: tempDir });
|
|
198
|
+
expect(io.stderr()).toBe('Run in a TTY, or pass --target <target>.\n');
|
|
199
|
+
});
|
|
166
200
|
it('prints standalone agent next actions after successful installation', async () => {
|
|
167
201
|
const io = makeIo();
|
|
168
202
|
const result = await runKtxSetupAgentsStep({
|
|
@@ -49,6 +49,7 @@ export interface KtxSetupEmbeddingsDeps {
|
|
|
49
49
|
healthCheck?: (config: KtxEmbeddingConfig) => Promise<KtxEmbeddingHealthCheckResult>;
|
|
50
50
|
ensureLocalEmbeddings?: (options: {
|
|
51
51
|
cliVersion: string;
|
|
52
|
+
projectDir: string;
|
|
52
53
|
installPolicy: KtxManagedPythonInstallPolicy;
|
|
53
54
|
io: KtxCliIo;
|
|
54
55
|
}) => Promise<ManagedLocalEmbeddingsDaemon>;
|
package/dist/setup-embeddings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { resolveKtxConfigReference } from '@ktx/context/core';
|
|
3
3
|
import { loadKtxProject, markKtxSetupStateStepComplete, readKtxSetupState, serializeKtxProjectConfig, } from '@ktx/context/project';
|
|
4
4
|
import { runKtxEmbeddingHealthCheck } from '@ktx/llm';
|
|
@@ -20,13 +20,13 @@ const LOCAL_EMBEDDING_BACKEND = 'sentence-transformers';
|
|
|
20
20
|
const EMBEDDING_OPTION_PROMPT_CONTEXT = 'KTX uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
|
|
21
21
|
'and relationship evidence.';
|
|
22
22
|
const LOCAL_EMBEDDING_HEALTH_TIMEOUT_MS = 120_000;
|
|
23
|
+
const LOCAL_EMBEDDING_STDERR_TAIL_LINES = 40;
|
|
23
24
|
function createPromptAdapter() {
|
|
24
25
|
return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
|
|
25
26
|
}
|
|
26
27
|
async function hasCompletedEmbeddings(projectDir, config) {
|
|
27
28
|
return ((await readKtxSetupState(projectDir)).completed_steps.includes('embeddings') &&
|
|
28
29
|
config.ingest.embeddings.backend !== 'none' &&
|
|
29
|
-
config.ingest.embeddings.backend !== 'deterministic' &&
|
|
30
30
|
typeof config.ingest.embeddings.model === 'string' &&
|
|
31
31
|
config.ingest.embeddings.model.length > 0 &&
|
|
32
32
|
config.ingest.embeddings.dimensions > 0);
|
|
@@ -190,14 +190,33 @@ async function chooseEmbeddingBackend(args, deps) {
|
|
|
190
190
|
}
|
|
191
191
|
return 'back';
|
|
192
192
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
193
|
+
async function readLocalEmbeddingDaemonStderrTail(stderrLog) {
|
|
194
|
+
if (!stderrLog) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const lines = (await readFile(stderrLog, 'utf8'))
|
|
199
|
+
.split(/\r?\n/)
|
|
200
|
+
.map((line) => line.trimEnd())
|
|
201
|
+
.filter((line) => line.trim().length > 0);
|
|
202
|
+
return lines.slice(-LOCAL_EMBEDDING_STDERR_TAIL_LINES);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function localEmbeddingSetupMessage(message, stderrTail = []) {
|
|
209
|
+
const lines = [
|
|
195
210
|
`Local embedding health check failed: ${message}`,
|
|
196
211
|
'Local embeddings use the KTX-managed Python runtime.',
|
|
197
|
-
'Prepare the runtime with: ktx
|
|
212
|
+
'Prepare the runtime with: ktx admin runtime start --feature local-embeddings',
|
|
198
213
|
'Use --yes with setup to install and start the runtime without prompting.',
|
|
199
214
|
'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
|
|
200
|
-
]
|
|
215
|
+
];
|
|
216
|
+
if (stderrTail.length > 0) {
|
|
217
|
+
lines.push('Recent local embeddings daemon stderr:', ...stderrTail);
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
201
220
|
}
|
|
202
221
|
async function promptAfterLocalEmbeddingFailure(deps) {
|
|
203
222
|
const choice = await (deps.prompts ?? createPromptAdapter()).select({
|
|
@@ -324,8 +343,11 @@ export async function runKtxSetupEmbeddingsStep(args, io, deps = {}) {
|
|
|
324
343
|
return { status: 'ready', projectDir: args.projectDir };
|
|
325
344
|
}
|
|
326
345
|
progress.fail('Embedding test failed');
|
|
346
|
+
const stderrTail = selectedBackend === 'sentence-transformers'
|
|
347
|
+
? await readLocalEmbeddingDaemonStderrTail(managedLocalEmbeddings?.stderrLog)
|
|
348
|
+
: [];
|
|
327
349
|
io.stderr.write(selectedBackend === 'sentence-transformers'
|
|
328
|
-
? `${localEmbeddingSetupMessage(health.message)}\n`
|
|
350
|
+
? `${localEmbeddingSetupMessage(health.message, stderrTail)}\n`
|
|
329
351
|
: `Embedding health check failed: ${health.message}\n`);
|
|
330
352
|
if (args.inputMode === 'disabled') {
|
|
331
353
|
return { status: 'failed', projectDir: args.projectDir };
|
|
@@ -39,9 +39,11 @@ function makePromptAdapter(options) {
|
|
|
39
39
|
cancel: vi.fn(),
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
function managedDaemon(baseUrl = 'http://127.0.0.1:61234') {
|
|
42
|
+
function managedDaemon(baseUrl = 'http://127.0.0.1:61234', logs = {}) {
|
|
43
43
|
return {
|
|
44
44
|
baseUrl,
|
|
45
|
+
stdoutLog: logs.stdoutLog ?? '/tmp/ktx-daemon.stdout.log',
|
|
46
|
+
stderrLog: logs.stderrLog ?? '/tmp/ktx-daemon.stderr.log',
|
|
45
47
|
env: {
|
|
46
48
|
KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: baseUrl,
|
|
47
49
|
},
|
|
@@ -223,7 +225,7 @@ describe('setup embeddings step', () => {
|
|
|
223
225
|
it('fails non-interactive local setup when the managed local embeddings runtime is missing', async () => {
|
|
224
226
|
const io = makeIo();
|
|
225
227
|
const ensureLocalEmbeddings = vi.fn(async () => {
|
|
226
|
-
throw new Error('KTX Python runtime is required for this command. Run: ktx
|
|
228
|
+
throw new Error('KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes');
|
|
227
229
|
});
|
|
228
230
|
const result = await runKtxSetupEmbeddingsStep({
|
|
229
231
|
projectDir: tempDir,
|
|
@@ -233,7 +235,7 @@ describe('setup embeddings step', () => {
|
|
|
233
235
|
skipEmbeddings: false,
|
|
234
236
|
}, io.io, { env: {}, ensureLocalEmbeddings });
|
|
235
237
|
expect(result.status).toBe('failed');
|
|
236
|
-
expect(io.stderr()).toContain('KTX Python runtime is required for this command. Run: ktx
|
|
238
|
+
expect(io.stderr()).toContain('KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes');
|
|
237
239
|
});
|
|
238
240
|
it('does not persist embedding completion when the health check fails', async () => {
|
|
239
241
|
const io = makeIo();
|
|
@@ -251,11 +253,51 @@ describe('setup embeddings step', () => {
|
|
|
251
253
|
expect(result.status).toBe('failed');
|
|
252
254
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
253
255
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
254
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
256
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
255
257
|
expect(io.stderr()).toContain('Local embedding health check failed: 401 invalid api key [redacted]');
|
|
256
|
-
expect(io.stderr()).toContain('Prepare the runtime with: ktx
|
|
258
|
+
expect(io.stderr()).toContain('Prepare the runtime with: ktx admin runtime start --feature local-embeddings');
|
|
257
259
|
expect(io.stderr()).not.toContain('skip for now');
|
|
258
260
|
});
|
|
261
|
+
it('prints the recent daemon stderr tail when local embedding health check fails', async () => {
|
|
262
|
+
const io = makeIo();
|
|
263
|
+
const stderrLog = join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log');
|
|
264
|
+
await mkdir(join(tempDir, '.ktx', 'runtime'), { recursive: true });
|
|
265
|
+
await writeFile(stderrLog, Array.from({ length: 45 }, (_value, index) => `daemon traceback line ${index + 1}`).join('\n'));
|
|
266
|
+
const result = await runKtxSetupEmbeddingsStep({
|
|
267
|
+
projectDir: tempDir,
|
|
268
|
+
inputMode: 'disabled',
|
|
269
|
+
cliVersion: '0.2.0',
|
|
270
|
+
runtimeInstallPolicy: 'auto',
|
|
271
|
+
skipEmbeddings: false,
|
|
272
|
+
}, io.io, {
|
|
273
|
+
env: {},
|
|
274
|
+
ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', { stderrLog })),
|
|
275
|
+
healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
|
|
276
|
+
});
|
|
277
|
+
expect(result.status).toBe('failed');
|
|
278
|
+
expect(io.stderr()).toContain('Recent local embeddings daemon stderr:');
|
|
279
|
+
expect(io.stderr()).toContain('daemon traceback line 6');
|
|
280
|
+
expect(io.stderr()).toContain('daemon traceback line 45');
|
|
281
|
+
expect(io.stderr()).not.toContain('daemon traceback line 5');
|
|
282
|
+
});
|
|
283
|
+
it('does not print daemon stderr diagnostics when the log is unavailable or empty', async () => {
|
|
284
|
+
const io = makeIo();
|
|
285
|
+
const result = await runKtxSetupEmbeddingsStep({
|
|
286
|
+
projectDir: tempDir,
|
|
287
|
+
inputMode: 'disabled',
|
|
288
|
+
cliVersion: '0.2.0',
|
|
289
|
+
runtimeInstallPolicy: 'auto',
|
|
290
|
+
skipEmbeddings: false,
|
|
291
|
+
}, io.io, {
|
|
292
|
+
env: {},
|
|
293
|
+
ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', {
|
|
294
|
+
stderrLog: join(tempDir, '.ktx', 'runtime', 'missing.stderr.log'),
|
|
295
|
+
})),
|
|
296
|
+
healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
|
|
297
|
+
});
|
|
298
|
+
expect(result.status).toBe('failed');
|
|
299
|
+
expect(io.stderr()).not.toContain('Recent local embeddings daemon stderr:');
|
|
300
|
+
});
|
|
259
301
|
it('uses fixed OpenAI defaults and only asks for credentials when OpenAI is selected', async () => {
|
|
260
302
|
const io = makeIo();
|
|
261
303
|
const healthCheck = vi.fn(async () => ({ ok: true }));
|
|
@@ -342,7 +384,7 @@ describe('setup embeddings step', () => {
|
|
|
342
384
|
expect(result.status).toBe('skipped');
|
|
343
385
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
344
386
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
345
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
387
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
346
388
|
});
|
|
347
389
|
it('returns back without writing config when the local health check fails and Back is selected', async () => {
|
|
348
390
|
const prompts = makePromptAdapter({ selectValues: ['sentence-transformers', 'back'] });
|
|
@@ -360,7 +402,7 @@ describe('setup embeddings step', () => {
|
|
|
360
402
|
});
|
|
361
403
|
expect(result.status).toBe('back');
|
|
362
404
|
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
|
363
|
-
expect(config.ingest.embeddings.backend).toBe('
|
|
405
|
+
expect(config.ingest.embeddings.backend).toBe('none');
|
|
364
406
|
});
|
|
365
407
|
it('preserves already completed embeddings setup when no embedding args request changes', async () => {
|
|
366
408
|
await mkdir(join(tempDir, '.ktx'), { recursive: true });
|
package/dist/setup-models.d.ts
CHANGED
package/dist/setup-models.js
CHANGED
|
@@ -312,13 +312,13 @@ function requestedBackend(args) {
|
|
|
312
312
|
if (args.vertexProject || args.vertexLocation) {
|
|
313
313
|
return 'vertex';
|
|
314
314
|
}
|
|
315
|
-
if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel
|
|
315
|
+
if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel) {
|
|
316
316
|
return 'anthropic';
|
|
317
317
|
}
|
|
318
318
|
return undefined;
|
|
319
319
|
}
|
|
320
320
|
function requestedModel(args) {
|
|
321
|
-
return args.llmModel
|
|
321
|
+
return args.llmModel;
|
|
322
322
|
}
|
|
323
323
|
async function chooseBackend(args, io, deps) {
|
|
324
324
|
const explicit = requestedBackend(args);
|
|
@@ -701,7 +701,6 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
|
|
|
701
701
|
!args.anthropicApiKeyEnv &&
|
|
702
702
|
!args.anthropicApiKeyFile &&
|
|
703
703
|
!args.llmModel &&
|
|
704
|
-
!args.anthropicModel &&
|
|
705
704
|
!args.vertexProject &&
|
|
706
705
|
!args.vertexLocation) {
|
|
707
706
|
io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
|
|
@@ -216,7 +216,7 @@ describe('setup Anthropic model step', () => {
|
|
|
216
216
|
projectDir: tempDir,
|
|
217
217
|
inputMode: 'disabled',
|
|
218
218
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
|
219
|
-
|
|
219
|
+
llmModel: 'claude-sonnet-4-6',
|
|
220
220
|
skipLlm: false,
|
|
221
221
|
}, io.io, {
|
|
222
222
|
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
|
@@ -253,7 +253,7 @@ describe('setup Anthropic model step', () => {
|
|
|
253
253
|
llmBackend: 'vertex',
|
|
254
254
|
vertexProject: 'local-gcp-project',
|
|
255
255
|
vertexLocation: 'us-east5',
|
|
256
|
-
|
|
256
|
+
llmModel: 'claude-sonnet-4-6',
|
|
257
257
|
skipLlm: false,
|
|
258
258
|
}, io.io, { env: {}, healthCheck, spinner });
|
|
259
259
|
expect(result.status).toBe('ready');
|
|
@@ -476,7 +476,7 @@ describe('setup Anthropic model step', () => {
|
|
|
476
476
|
llmBackend: 'vertex',
|
|
477
477
|
vertexProject: 'kaelio-orbit-looker-20260430',
|
|
478
478
|
vertexLocation: 'us-east5',
|
|
479
|
-
|
|
479
|
+
llmModel: 'claude-sonnet-4-6',
|
|
480
480
|
skipLlm: false,
|
|
481
481
|
}, io.io, {
|
|
482
482
|
env: {},
|
|
@@ -497,7 +497,7 @@ describe('setup Anthropic model step', () => {
|
|
|
497
497
|
projectDir: tempDir,
|
|
498
498
|
inputMode: 'disabled',
|
|
499
499
|
anthropicApiKeyFile: secretPath,
|
|
500
|
-
|
|
500
|
+
llmModel: 'claude-sonnet-4-6',
|
|
501
501
|
skipLlm: false,
|
|
502
502
|
}, io.io, { env: {}, healthCheck });
|
|
503
503
|
expect(result.status).toBe('ready');
|
|
@@ -525,7 +525,7 @@ describe('setup Anthropic model step', () => {
|
|
|
525
525
|
projectDir: tempDir,
|
|
526
526
|
inputMode: 'disabled',
|
|
527
527
|
anthropicApiKeyFile: missingSecretPath,
|
|
528
|
-
|
|
528
|
+
llmModel: 'claude-sonnet-4-6',
|
|
529
529
|
skipLlm: false,
|
|
530
530
|
}, io.io, { env: {}, healthCheck });
|
|
531
531
|
expect(result.status).toBe('missing-input');
|
|
@@ -720,7 +720,7 @@ describe('setup Anthropic model step', () => {
|
|
|
720
720
|
projectDir: tempDir,
|
|
721
721
|
inputMode: 'disabled',
|
|
722
722
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
|
723
|
-
|
|
723
|
+
llmModel: 'claude-sonnet-4-6',
|
|
724
724
|
skipLlm: false,
|
|
725
725
|
}, io.io, {
|
|
726
726
|
env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
|
|
@@ -830,8 +830,7 @@ describe('setup Anthropic model step', () => {
|
|
|
830
830
|
' default: claude-sonnet-4-6',
|
|
831
831
|
'ingest:',
|
|
832
832
|
' embeddings:',
|
|
833
|
-
' backend:
|
|
834
|
-
' model: deterministic',
|
|
833
|
+
' backend: none',
|
|
835
834
|
' dimensions: 8',
|
|
836
835
|
].join('\n'), 'utf-8');
|
|
837
836
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
|
|
@@ -865,8 +864,7 @@ describe('setup Anthropic model step', () => {
|
|
|
865
864
|
` default: ${fixture.model}`,
|
|
866
865
|
'ingest:',
|
|
867
866
|
' embeddings:',
|
|
868
|
-
' backend:
|
|
869
|
-
' model: deterministic',
|
|
867
|
+
' backend: none',
|
|
870
868
|
' dimensions: 8',
|
|
871
869
|
].join('\n'), 'utf-8');
|
|
872
870
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
|
package/dist/setup-project.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { initKtxProject, type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
|
|
2
2
|
import type { KtxCliIo } from './cli-runtime.js';
|
|
3
3
|
import { type KtxSetupPromptOption } from './setup-prompts.js';
|
|
4
|
-
export type KtxSetupProjectMode = 'auto' | '
|
|
4
|
+
export type KtxSetupProjectMode = 'auto' | 'prompt-new';
|
|
5
5
|
export type KtxSetupInputMode = 'auto' | 'disabled';
|
|
6
6
|
export interface KtxSetupProjectArgs {
|
|
7
7
|
projectDir: string;
|
|
@@ -10,11 +10,19 @@ export interface KtxSetupProjectArgs {
|
|
|
10
10
|
yes: boolean;
|
|
11
11
|
allowBack?: boolean;
|
|
12
12
|
}
|
|
13
|
+
export type KtxSetupCreatedProjectCleanup = {
|
|
14
|
+
kind: 'remove-project-dir';
|
|
15
|
+
projectDir: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'remove-ktx-scaffold';
|
|
18
|
+
projectDir: string;
|
|
19
|
+
};
|
|
13
20
|
export type KtxSetupProjectResult = {
|
|
14
21
|
status: 'ready';
|
|
15
22
|
projectDir: string;
|
|
16
23
|
project: KtxLocalProject;
|
|
17
24
|
confirmedCreation?: boolean;
|
|
25
|
+
createdProjectCleanup?: KtxSetupCreatedProjectCleanup;
|
|
18
26
|
} | {
|
|
19
27
|
status: 'back';
|
|
20
28
|
projectDir: string;
|
package/dist/setup-project.js
CHANGED
|
@@ -37,6 +37,15 @@ async function existingFolderState(projectDir) {
|
|
|
37
37
|
throw error;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
function cleanupForFolderState(projectDir, state) {
|
|
41
|
+
if (state === 'missing') {
|
|
42
|
+
return { kind: 'remove-project-dir', projectDir };
|
|
43
|
+
}
|
|
44
|
+
if (state === 'empty-directory') {
|
|
45
|
+
return { kind: 'remove-ktx-scaffold', projectDir };
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
40
49
|
async function confirmProjectDir(selectedDir, io, prompts) {
|
|
41
50
|
const state = await existingFolderState(selectedDir);
|
|
42
51
|
if (state === 'not-directory') {
|
|
@@ -75,7 +84,7 @@ async function confirmProjectDir(selectedDir, io, prompts) {
|
|
|
75
84
|
return { status: 'back' };
|
|
76
85
|
if (action !== 'create')
|
|
77
86
|
return { status: 'cancelled' };
|
|
78
|
-
return { status: 'confirmed', confirmedCreation: true };
|
|
87
|
+
return { status: 'confirmed', confirmedCreation: true, createdProjectCleanup: cleanupForFolderState(selectedDir, state) };
|
|
79
88
|
}
|
|
80
89
|
async function normalizeSetupGitignore(projectDir) {
|
|
81
90
|
const gitignorePath = join(projectDir, '.ktx/.gitignore');
|
|
@@ -147,34 +156,34 @@ async function promptForNewProjectDir(projectDir, homeDir, io, prompts) {
|
|
|
147
156
|
return { status: 'back', projectDir };
|
|
148
157
|
if (confirmed.status === 'cancelled')
|
|
149
158
|
return { status: 'cancelled', projectDir };
|
|
150
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
status: 'selected',
|
|
161
|
+
projectDir: selectedDir,
|
|
162
|
+
confirmedCreation: confirmed.confirmedCreation,
|
|
163
|
+
...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
|
|
164
|
+
};
|
|
151
165
|
}
|
|
152
166
|
}
|
|
167
|
+
async function createProjectWithCleanup(projectDir, deps) {
|
|
168
|
+
const state = await existingFolderState(projectDir);
|
|
169
|
+
const project = await createProject(projectDir, deps);
|
|
170
|
+
const createdProjectCleanup = cleanupForFolderState(projectDir, state);
|
|
171
|
+
return {
|
|
172
|
+
project,
|
|
173
|
+
...(createdProjectCleanup ? { createdProjectCleanup } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
153
176
|
export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
154
177
|
const projectDir = resolve(args.projectDir);
|
|
155
178
|
const homeDir = deps.homeDir ?? homedir();
|
|
156
179
|
const exists = hasProjectConfig(projectDir);
|
|
157
|
-
if (args.mode === 'existing') {
|
|
158
|
-
if (!exists) {
|
|
159
|
-
io.stderr.write(`No existing KTX project found at ${projectDir}. Pass --new to create it.\n`);
|
|
160
|
-
return { status: 'missing-input', projectDir };
|
|
161
|
-
}
|
|
162
|
-
const project = await loadExistingProject(projectDir, deps);
|
|
163
|
-
printProjectSummary(io, projectDir);
|
|
164
|
-
return { status: 'ready', projectDir, project };
|
|
165
|
-
}
|
|
166
|
-
if (args.mode === 'new') {
|
|
167
|
-
const project = await createProject(projectDir, deps);
|
|
168
|
-
printProjectSummary(io, projectDir);
|
|
169
|
-
return { status: 'ready', projectDir, project };
|
|
170
|
-
}
|
|
171
180
|
if (args.mode === 'prompt-new') {
|
|
172
181
|
if (args.inputMode === 'disabled') {
|
|
173
|
-
io.stderr.write('Missing new project folder: pass --
|
|
182
|
+
io.stderr.write('Missing new project folder: pass --project-dir and --yes to create a project without prompts.\n');
|
|
174
183
|
return { status: 'missing-input', projectDir };
|
|
175
184
|
}
|
|
176
185
|
if (!io.stdout.isTTY && !deps.prompts) {
|
|
177
|
-
io.stderr.write('Missing new project folder: pass --
|
|
186
|
+
io.stderr.write('Missing new project folder: pass --project-dir and --yes to create a project outside an interactive terminal.\n');
|
|
178
187
|
return { status: 'missing-input', projectDir };
|
|
179
188
|
}
|
|
180
189
|
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
|
@@ -192,6 +201,7 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
|
192
201
|
projectDir: selected.projectDir,
|
|
193
202
|
project,
|
|
194
203
|
confirmedCreation: selected.confirmedCreation,
|
|
204
|
+
...(selected.createdProjectCleanup ? { createdProjectCleanup: selected.createdProjectCleanup } : {}),
|
|
195
205
|
};
|
|
196
206
|
}
|
|
197
207
|
if (exists) {
|
|
@@ -201,15 +211,20 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
|
201
211
|
}
|
|
202
212
|
if (args.inputMode === 'disabled') {
|
|
203
213
|
if (!args.yes) {
|
|
204
|
-
io.stderr.write('Missing setup choice: pass --
|
|
214
|
+
io.stderr.write('Missing setup choice: pass --yes to create a project in non-interactive setup.\n');
|
|
205
215
|
return { status: 'missing-input', projectDir };
|
|
206
216
|
}
|
|
207
|
-
const project = await
|
|
217
|
+
const { project, createdProjectCleanup } = await createProjectWithCleanup(projectDir, deps);
|
|
208
218
|
printProjectSummary(io, projectDir);
|
|
209
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
status: 'ready',
|
|
221
|
+
projectDir,
|
|
222
|
+
project,
|
|
223
|
+
...(createdProjectCleanup ? { createdProjectCleanup } : {}),
|
|
224
|
+
};
|
|
210
225
|
}
|
|
211
226
|
if (!io.stdout.isTTY && !deps.prompts) {
|
|
212
|
-
io.stderr.write('Missing setup choice: pass --
|
|
227
|
+
io.stderr.write('Missing setup choice: pass --yes to create a project outside an interactive terminal.\n');
|
|
213
228
|
return { status: 'missing-input', projectDir };
|
|
214
229
|
}
|
|
215
230
|
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
|
@@ -238,9 +253,14 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
|
238
253
|
return { status: 'cancelled', projectDir };
|
|
239
254
|
}
|
|
240
255
|
if (choice === 'current') {
|
|
241
|
-
const project = await
|
|
256
|
+
const { project, createdProjectCleanup } = await createProjectWithCleanup(projectDir, deps);
|
|
242
257
|
printProjectSummary(io, projectDir);
|
|
243
|
-
return {
|
|
258
|
+
return {
|
|
259
|
+
status: 'ready',
|
|
260
|
+
projectDir,
|
|
261
|
+
project,
|
|
262
|
+
...(createdProjectCleanup ? { createdProjectCleanup } : {}),
|
|
263
|
+
};
|
|
244
264
|
}
|
|
245
265
|
if (choice === 'new-default') {
|
|
246
266
|
const confirmed = await confirmProjectDir(defaultProjectDir, io, prompts);
|
|
@@ -257,6 +277,7 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
|
257
277
|
projectDir: defaultProjectDir,
|
|
258
278
|
project,
|
|
259
279
|
confirmedCreation: confirmed.confirmedCreation,
|
|
280
|
+
...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
|
|
260
281
|
};
|
|
261
282
|
}
|
|
262
283
|
if (choice === 'new-custom') {
|
|
@@ -281,7 +302,13 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
|
|
|
281
302
|
return { status: 'cancelled', projectDir };
|
|
282
303
|
const project = await createProject(customDir, deps);
|
|
283
304
|
printProjectSummary(io, customDir);
|
|
284
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
status: 'ready',
|
|
307
|
+
projectDir: customDir,
|
|
308
|
+
project,
|
|
309
|
+
confirmedCreation: confirmed.confirmedCreation,
|
|
310
|
+
...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
|
|
311
|
+
};
|
|
285
312
|
}
|
|
286
313
|
prompts.cancel('Setup cancelled.');
|
|
287
314
|
return { status: 'cancelled', projectDir };
|
|
@@ -48,10 +48,10 @@ describe('setup project step', () => {
|
|
|
48
48
|
afterEach(async () => {
|
|
49
49
|
await rm(tempDir, { recursive: true, force: true });
|
|
50
50
|
});
|
|
51
|
-
it('creates a new project with --
|
|
51
|
+
it('creates a new project in non-interactive auto mode with --yes and marks the project step complete', async () => {
|
|
52
52
|
const projectDir = join(tempDir, 'warehouse');
|
|
53
53
|
const testIo = makeIo();
|
|
54
|
-
const result = await runKtxSetupProjectStep({ projectDir, mode: '
|
|
54
|
+
const result = await runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: true }, testIo.io);
|
|
55
55
|
expect(result.status).toBe('ready');
|
|
56
56
|
expect(result.projectDir).toBe(projectDir);
|
|
57
57
|
expect(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
@@ -61,7 +61,7 @@ describe('setup project step', () => {
|
|
|
61
61
|
expect(testIo.stdout()).toContain(`Project: ${projectDir}`);
|
|
62
62
|
expect(testIo.stderr()).toBe('');
|
|
63
63
|
});
|
|
64
|
-
it('loads an existing project
|
|
64
|
+
it('loads an existing project in auto mode and drops config setup progress', async () => {
|
|
65
65
|
const projectDir = join(tempDir, 'warehouse');
|
|
66
66
|
await initKtxProject({ projectDir });
|
|
67
67
|
await writeFile(join(projectDir, 'ktx.yaml'), [
|
|
@@ -72,7 +72,7 @@ describe('setup project step', () => {
|
|
|
72
72
|
' - llm',
|
|
73
73
|
'connections: {}',
|
|
74
74
|
].join('\n'), 'utf-8');
|
|
75
|
-
const result = await runKtxSetupProjectStep({ projectDir, mode: '
|
|
75
|
+
const result = await runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, makeIo().io);
|
|
76
76
|
expect(result.status).toBe('ready');
|
|
77
77
|
const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
|
78
78
|
expect(config.setup).toEqual({
|
|
@@ -86,16 +86,16 @@ describe('setup project step', () => {
|
|
|
86
86
|
const rejectedIo = makeIo();
|
|
87
87
|
const acceptedIo = makeIo();
|
|
88
88
|
await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, rejectedIo.io)).resolves.toMatchObject({ status: 'missing-input' });
|
|
89
|
-
expect(rejectedIo.stderr()).toContain('Missing setup choice: pass --
|
|
89
|
+
expect(rejectedIo.stderr()).toContain('Missing setup choice: pass --yes');
|
|
90
90
|
await expect(stat(join(projectDir, 'ktx.yaml'))).rejects.toThrow();
|
|
91
91
|
await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: true }, acceptedIo.io)).resolves.toMatchObject({ status: 'ready', projectDir });
|
|
92
92
|
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
93
93
|
});
|
|
94
|
-
it('fails
|
|
94
|
+
it('fails clearly in no-input auto mode when ktx.yaml is missing and --yes is absent', async () => {
|
|
95
95
|
const projectDir = join(tempDir, 'warehouse');
|
|
96
96
|
const testIo = makeIo();
|
|
97
|
-
await expect(runKtxSetupProjectStep({ projectDir, mode: '
|
|
98
|
-
expect(testIo.stderr()).toContain(
|
|
97
|
+
await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, testIo.io)).resolves.toMatchObject({ status: 'missing-input' });
|
|
98
|
+
expect(testIo.stderr()).toContain('Missing setup choice: pass --yes');
|
|
99
99
|
});
|
|
100
100
|
it('prompts to use the current directory and creates a project in interactive auto mode', async () => {
|
|
101
101
|
const projectDir = join(tempDir, 'warehouse');
|
|
@@ -56,7 +56,7 @@ describe('runKtxSetupRuntimeStep', () => {
|
|
|
56
56
|
it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
|
|
57
57
|
const io = makeIo();
|
|
58
58
|
const ensureRuntime = vi.fn(async () => {
|
|
59
|
-
throw new Error('KTX Python runtime is required for this command. Run: ktx
|
|
59
|
+
throw new Error('KTX Python runtime is required for this command. Run: ktx admin runtime install --yes');
|
|
60
60
|
});
|
|
61
61
|
await expect(runKtxSetupRuntimeStep({
|
|
62
62
|
projectDir: tempDir,
|
|
@@ -71,12 +71,14 @@ describe('runKtxSetupRuntimeStep', () => {
|
|
|
71
71
|
})).resolves.toMatchObject({ status: 'failed' });
|
|
72
72
|
expect(ensureRuntime).toHaveBeenCalledWith(expect.objectContaining({ installPolicy: 'never' }));
|
|
73
73
|
expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('runtime');
|
|
74
|
-
expect(io.stderr()).toContain('ktx
|
|
74
|
+
expect(io.stderr()).toContain('ktx admin runtime install --yes');
|
|
75
75
|
});
|
|
76
76
|
it('starts the managed local embeddings daemon for configured sentence-transformers embeddings', async () => {
|
|
77
77
|
const io = makeIo();
|
|
78
78
|
const ensureLocalEmbeddings = vi.fn(async () => ({
|
|
79
79
|
baseUrl: 'http://127.0.0.1:61234',
|
|
80
|
+
stdoutLog: join(tempDir, '.ktx', 'runtime', 'daemon.stdout.log'),
|
|
81
|
+
stderrLog: join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log'),
|
|
80
82
|
env: { KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: 'http://127.0.0.1:61234' },
|
|
81
83
|
}));
|
|
82
84
|
const config = {
|