@kaelio/ktx 0.1.1 → 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.1-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/sl-commands.js +27 -32
- package/dist/doctor.test.js +4 -4
- package/dist/example-smoke.test.js +3 -3
- package/dist/index.test.js +76 -60
- package/dist/io/print-list.test.js +4 -4
- package/dist/knowledge.js +1 -1
- package/dist/managed-python-command.js +2 -2
- package/dist/managed-python-command.test.js +3 -3
- package/dist/managed-python-runtime.d.ts +1 -1
- package/dist/managed-python-runtime.js +3 -3
- package/dist/managed-python-runtime.test.js +2 -2
- 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/public-ingest.js +3 -5
- package/dist/public-ingest.test.js +7 -3
- package/dist/runtime.test.js +1 -1
- package/dist/scan.test.js +2 -2
- package/dist/setup-agents.js +3 -3
- package/dist/setup-agents.test.js +1 -1
- package/dist/setup-embeddings.js +1 -1
- package/dist/setup-embeddings.test.js +3 -3
- package/dist/setup-runtime.test.js +2 -2
- package/dist/sl.js +1 -1
- package/dist/standalone-smoke.test.js +6 -2
- 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/local-bundle-runtime.js +3 -0
- 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/memory/local-memory.js +9 -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/package.json +1 -1
- /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
package/assets/python/{kaelio_ktx-0.1.1-py3-none-any.whl → kaelio_ktx-0.2.0-py3-none-any.whl}
RENAMED
|
Binary file
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"distributionName": "kaelio-ktx",
|
|
4
4
|
"normalizedName": "kaelio_ktx",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.0",
|
|
6
6
|
"wheel": {
|
|
7
|
-
"file": "kaelio_ktx-0.
|
|
8
|
-
"sha256": "
|
|
9
|
-
"bytes":
|
|
7
|
+
"file": "kaelio_ktx-0.2.0-py3-none-any.whl",
|
|
8
|
+
"sha256": "5ede628d4d72b2a7eb515fb538473be5a5d82eb430ae98daca8ac90596a7a6e3",
|
|
9
|
+
"bytes": 80524
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReindexSummary } from '@ktx/context/index-sync';
|
|
2
|
+
import { type Command } from '@commander-js/extra-typings';
|
|
3
|
+
import type { KtxCliCommandContext } from './cli-program.js';
|
|
4
|
+
import type { KtxCliIo } from './cli-runtime.js';
|
|
5
|
+
export interface KtxAdminReindexArgs {
|
|
6
|
+
projectDir: string;
|
|
7
|
+
force: boolean;
|
|
8
|
+
output?: 'pretty' | 'plain' | 'json';
|
|
9
|
+
json?: boolean;
|
|
10
|
+
cliVersion: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerAdminReindexCommand(admin: Command, context: KtxCliCommandContext): void;
|
|
13
|
+
export declare function reindexHasErrors(summary: ReindexSummary): boolean;
|
|
14
|
+
export declare function renderReindexPlain(summary: ReindexSummary, io: KtxCliIo): void;
|
|
15
|
+
export declare function renderReindexJson(summary: ReindexSummary, io: KtxCliIo): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapter, MANAGED_SENTENCE_TRANSFORMERS_BASE_URL, } from '@ktx/context';
|
|
2
|
+
import { reindexLocalIndexes } from '@ktx/context/index-sync';
|
|
3
|
+
import { loadKtxProject } from '@ktx/context/project';
|
|
4
|
+
import { Option } from '@commander-js/extra-typings';
|
|
5
|
+
import { cancel, intro, log, note, outro } from '@clack/prompts';
|
|
6
|
+
import { resolveOutputMode } from './io/mode.js';
|
|
7
|
+
import { green, red, SYMBOLS } from './io/symbols.js';
|
|
8
|
+
import { ensureManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js';
|
|
9
|
+
export function registerAdminReindexCommand(admin, context) {
|
|
10
|
+
admin
|
|
11
|
+
.command('reindex')
|
|
12
|
+
.description('Sync local wiki and semantic-layer search indexes from disk')
|
|
13
|
+
.option('--force', 'Clear each discovered scope before rebuilding it', false)
|
|
14
|
+
.option('--json', 'Shortcut for --output=json (overrides --output)', false)
|
|
15
|
+
.addOption(new Option('--output <mode>', 'Output mode: pretty, plain, or json').choices(['pretty', 'plain', 'json']))
|
|
16
|
+
.action(async (options, command) => {
|
|
17
|
+
const runner = context.deps.adminReindex ?? runKtxAdminReindex;
|
|
18
|
+
const { resolveCommandProjectDir } = await import('./cli-program.js');
|
|
19
|
+
context.setExitCode(await runner({
|
|
20
|
+
projectDir: resolveCommandProjectDir(command),
|
|
21
|
+
force: options.force === true,
|
|
22
|
+
json: options.json === true,
|
|
23
|
+
output: options.output,
|
|
24
|
+
cliVersion: context.packageInfo.version,
|
|
25
|
+
}, context.io));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function resolveReindexEmbeddingService(project, args, io) {
|
|
29
|
+
const config = project.config.ingest.embeddings;
|
|
30
|
+
if (config.backend === 'none') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (config.backend === 'sentence-transformers' &&
|
|
34
|
+
config.sentenceTransformers?.base_url === MANAGED_SENTENCE_TRANSFORMERS_BASE_URL) {
|
|
35
|
+
const daemon = await ensureManagedLocalEmbeddingsDaemon({
|
|
36
|
+
cliVersion: args.cliVersion,
|
|
37
|
+
projectDir: project.projectDir,
|
|
38
|
+
installPolicy: 'never',
|
|
39
|
+
io,
|
|
40
|
+
});
|
|
41
|
+
const provider = createLocalKtxEmbeddingProviderFromConfig(config, { env: { ...process.env, ...daemon.env } });
|
|
42
|
+
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
|
|
43
|
+
}
|
|
44
|
+
const provider = createLocalKtxEmbeddingProviderFromConfig(config);
|
|
45
|
+
return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
|
|
46
|
+
}
|
|
47
|
+
function scopeKey(scope) {
|
|
48
|
+
if (scope.kind === 'wiki') {
|
|
49
|
+
return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
|
|
50
|
+
}
|
|
51
|
+
return `sl/${scope.connectionId ?? scope.label}`;
|
|
52
|
+
}
|
|
53
|
+
function quotePlainValue(value) {
|
|
54
|
+
return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
55
|
+
}
|
|
56
|
+
export function reindexHasErrors(summary) {
|
|
57
|
+
return summary.scopes.some((scope) => scope.error);
|
|
58
|
+
}
|
|
59
|
+
export function renderReindexPlain(summary, io) {
|
|
60
|
+
const updateKey = summary.force ? 'rebuilt' : 'updated';
|
|
61
|
+
for (const scope of summary.scopes) {
|
|
62
|
+
const cells = [
|
|
63
|
+
scopeKey(scope),
|
|
64
|
+
`scanned=${scope.scanned}`,
|
|
65
|
+
`${updateKey}=${scope.updated}`,
|
|
66
|
+
`deleted=${scope.deleted}`,
|
|
67
|
+
`embeddings=${summary.embeddingsAvailable ? String(scope.embeddingsRecomputed) : '-'}`,
|
|
68
|
+
`duration_ms=${scope.durationMs}`,
|
|
69
|
+
...(scope.error ? [`error=${quotePlainValue(scope.error)}`] : []),
|
|
70
|
+
];
|
|
71
|
+
io.stderr.write(`${cells.join('\t')}\n`);
|
|
72
|
+
}
|
|
73
|
+
const failed = summary.scopes.filter((scope) => scope.error).length;
|
|
74
|
+
io.stdout.write([
|
|
75
|
+
'reindex',
|
|
76
|
+
`scopes=${summary.scopes.length}`,
|
|
77
|
+
`scanned=${summary.totals.scanned}`,
|
|
78
|
+
`${updateKey}=${summary.totals.updated}`,
|
|
79
|
+
`deleted=${summary.totals.deleted}`,
|
|
80
|
+
`embeddings=${summary.embeddingsAvailable ? String(summary.totals.embeddingsRecomputed) : '-'}`,
|
|
81
|
+
`duration_ms=${summary.durationMs}`,
|
|
82
|
+
...(failed > 0 ? [`failed=${failed}`] : []),
|
|
83
|
+
].join('\t') + '\n');
|
|
84
|
+
}
|
|
85
|
+
export function renderReindexJson(summary, io) {
|
|
86
|
+
io.stdout.write(`${JSON.stringify({ kind: 'reindex', data: summary, meta: { command: 'admin reindex' } }, null, 2)}\n`);
|
|
87
|
+
}
|
|
88
|
+
function noun(scope) {
|
|
89
|
+
return scope.kind === 'wiki' ? 'pages' : 'sources';
|
|
90
|
+
}
|
|
91
|
+
function formatScopeLine(scope, force, embeddingsAvailable) {
|
|
92
|
+
if (scope.error) {
|
|
93
|
+
return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} failed: ${scope.error}`;
|
|
94
|
+
}
|
|
95
|
+
const changedLabel = force ? 'rebuilt' : 'updated';
|
|
96
|
+
const parts = [`${scope.scanned} ${noun(scope)}`];
|
|
97
|
+
if (scope.updated > 0) {
|
|
98
|
+
parts.push(`${scope.updated} ${changedLabel}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
parts.push('unchanged');
|
|
102
|
+
}
|
|
103
|
+
if (!force && scope.deleted > 0) {
|
|
104
|
+
parts.push(`${scope.deleted} deleted`);
|
|
105
|
+
}
|
|
106
|
+
if (embeddingsAvailable) {
|
|
107
|
+
parts.push(`${scope.embeddingsRecomputed} embeddings recomputed`);
|
|
108
|
+
}
|
|
109
|
+
parts.push(`${scope.durationMs}ms`);
|
|
110
|
+
return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} ${parts.join(` ${SYMBOLS.middot} `)}`;
|
|
111
|
+
}
|
|
112
|
+
function renderReindexPretty(summary, io) {
|
|
113
|
+
intro(summary.force ? 'ktx admin reindex --force' : 'ktx admin reindex');
|
|
114
|
+
if (!summary.embeddingsAvailable) {
|
|
115
|
+
log.warn(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only`);
|
|
116
|
+
}
|
|
117
|
+
for (const scope of summary.scopes) {
|
|
118
|
+
const line = formatScopeLine(scope, summary.force, summary.embeddingsAvailable);
|
|
119
|
+
if (scope.error) {
|
|
120
|
+
log.error(red(line));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
log.success(green(line));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const failed = summary.scopes.filter((scope) => scope.error).length;
|
|
127
|
+
note([
|
|
128
|
+
`scopes ${summary.scopes.length}`,
|
|
129
|
+
`scanned ${summary.totals.scanned}`,
|
|
130
|
+
`${summary.force ? 'rebuilt' : 'updated'} ${summary.totals.updated}`,
|
|
131
|
+
`deleted ${summary.totals.deleted}`,
|
|
132
|
+
`embeddings ${summary.embeddingsAvailable ? summary.totals.embeddingsRecomputed : SYMBOLS.emDash}`,
|
|
133
|
+
`index ${summary.dbPath}`,
|
|
134
|
+
...(failed > 0 ? [`failed ${failed}`] : []),
|
|
135
|
+
].join('\n'), 'Summary');
|
|
136
|
+
if (failed > 0) {
|
|
137
|
+
cancel(`reindex completed with ${failed} error${failed === 1 ? '' : 's'}`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
outro(`Done in ${(summary.durationMs / 1000).toFixed(1)}s`);
|
|
141
|
+
}
|
|
142
|
+
void io;
|
|
143
|
+
}
|
|
144
|
+
async function runKtxAdminReindex(args, io = process) {
|
|
145
|
+
try {
|
|
146
|
+
const project = await loadKtxProject({ projectDir: args.projectDir });
|
|
147
|
+
const embeddingService = await resolveReindexEmbeddingService(project, args, io);
|
|
148
|
+
const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
|
|
149
|
+
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
|
|
150
|
+
if (!summary.embeddingsAvailable && mode === 'plain') {
|
|
151
|
+
io.stderr.write(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only\n`);
|
|
152
|
+
}
|
|
153
|
+
if (mode === 'json') {
|
|
154
|
+
renderReindexJson(summary, io);
|
|
155
|
+
}
|
|
156
|
+
else if (mode === 'plain') {
|
|
157
|
+
renderReindexPlain(summary, io);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
renderReindexPretty(summary, io);
|
|
161
|
+
}
|
|
162
|
+
return reindexHasErrors(summary) ? 1 : 0;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { renderReindexJson, renderReindexPlain, reindexHasErrors } from './admin-reindex.js';
|
|
3
|
+
import { runKtxCli } from './index.js';
|
|
4
|
+
function makeIo(options = {}) {
|
|
5
|
+
let stdout = '';
|
|
6
|
+
let stderr = '';
|
|
7
|
+
return {
|
|
8
|
+
io: {
|
|
9
|
+
stdout: {
|
|
10
|
+
isTTY: options.stdoutIsTTY,
|
|
11
|
+
write: (chunk) => {
|
|
12
|
+
stdout += chunk;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
stderr: {
|
|
16
|
+
write: (chunk) => {
|
|
17
|
+
stderr += chunk;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
stdout: () => stdout,
|
|
22
|
+
stderr: () => stderr,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function summary(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
scopes: [
|
|
28
|
+
{
|
|
29
|
+
kind: 'wiki',
|
|
30
|
+
label: 'global',
|
|
31
|
+
scope: 'global',
|
|
32
|
+
scopeId: null,
|
|
33
|
+
scanned: 42,
|
|
34
|
+
updated: 3,
|
|
35
|
+
deleted: 1,
|
|
36
|
+
embeddingsRecomputed: 3,
|
|
37
|
+
embeddingsFailed: 0,
|
|
38
|
+
durationMs: 412,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: 'sl',
|
|
42
|
+
label: 'warehouse',
|
|
43
|
+
connectionId: 'warehouse',
|
|
44
|
+
scanned: 18,
|
|
45
|
+
updated: 2,
|
|
46
|
+
deleted: 0,
|
|
47
|
+
embeddingsRecomputed: 2,
|
|
48
|
+
embeddingsFailed: 0,
|
|
49
|
+
durationMs: 287,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
totals: { scanned: 60, updated: 5, deleted: 1, embeddingsRecomputed: 5, embeddingsFailed: 0 },
|
|
53
|
+
dbPath: '.ktx/db.sqlite',
|
|
54
|
+
force: false,
|
|
55
|
+
embeddingsAvailable: true,
|
|
56
|
+
durationMs: 1234,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
describe('admin reindex renderers', () => {
|
|
61
|
+
it('renders plain scope lines to stderr and summary to stdout', () => {
|
|
62
|
+
const io = makeIo();
|
|
63
|
+
renderReindexPlain(summary(), io.io);
|
|
64
|
+
expect(io.stderr()).toContain('wiki/global\tscanned=42\tupdated=3\tdeleted=1\tembeddings=3\tduration_ms=412\n');
|
|
65
|
+
expect(io.stderr()).toContain('sl/warehouse\tscanned=18\tupdated=2\tdeleted=0\tembeddings=2\tduration_ms=287\n');
|
|
66
|
+
expect(io.stdout()).toBe('reindex\tscopes=2\tscanned=60\tupdated=5\tdeleted=1\tembeddings=5\tduration_ms=1234\n');
|
|
67
|
+
});
|
|
68
|
+
it('renders rebuilt labels in plain force mode', () => {
|
|
69
|
+
const io = makeIo();
|
|
70
|
+
renderReindexPlain(summary({ force: true }), io.io);
|
|
71
|
+
expect(io.stderr()).toContain('rebuilt=3');
|
|
72
|
+
expect(io.stdout()).toContain('rebuilt=5');
|
|
73
|
+
expect(io.stdout()).not.toContain('updated=5');
|
|
74
|
+
});
|
|
75
|
+
it('renders json envelope to stdout only', () => {
|
|
76
|
+
const io = makeIo();
|
|
77
|
+
renderReindexJson(summary(), io.io);
|
|
78
|
+
expect(JSON.parse(io.stdout())).toMatchObject({
|
|
79
|
+
kind: 'reindex',
|
|
80
|
+
data: { totals: { scanned: 60, updated: 5 } },
|
|
81
|
+
meta: { command: 'admin reindex' },
|
|
82
|
+
});
|
|
83
|
+
expect(io.stderr()).toBe('');
|
|
84
|
+
});
|
|
85
|
+
it('detects per-scope errors', () => {
|
|
86
|
+
expect(reindexHasErrors(summary({
|
|
87
|
+
scopes: [{ ...summary().scopes[0], error: 'provider failed' }],
|
|
88
|
+
}))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('admin reindex Commander routing', () => {
|
|
92
|
+
it('routes flags to the injectable reindex runner', async () => {
|
|
93
|
+
const { mkdir, mkdtemp, rm, writeFile } = await import('node:fs/promises');
|
|
94
|
+
const { tmpdir } = await import('node:os');
|
|
95
|
+
const { join } = await import('node:path');
|
|
96
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-reindex-cli-'));
|
|
97
|
+
const projectDir = join(tempDir, 'project');
|
|
98
|
+
const io = makeIo();
|
|
99
|
+
const adminReindex = vi.fn(async () => 0);
|
|
100
|
+
try {
|
|
101
|
+
await mkdir(projectDir, { recursive: true });
|
|
102
|
+
await writeFile(join(projectDir, 'ktx.yaml'), '{}\n', 'utf-8');
|
|
103
|
+
await expect(runKtxCli(['--project-dir', projectDir, 'admin', 'reindex', '--force', '--json', '--output', 'plain'], io.io, { adminReindex })).resolves.toBe(0);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
expect(adminReindex).toHaveBeenCalledWith({
|
|
109
|
+
projectDir,
|
|
110
|
+
force: true,
|
|
111
|
+
json: true,
|
|
112
|
+
output: 'plain',
|
|
113
|
+
cliVersion: '0.1.0-rc.1',
|
|
114
|
+
}, io.io);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Command } from '@commander-js/extra-typings';
|
|
2
2
|
import { type KtxCliCommandContext } from './cli-program.js';
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function registerAdminCommands(program: Command, context: KtxCliCommandContext): void;
|
package/dist/{dev.js → admin.js}
RENAMED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { resolveCommandProjectDir } from './cli-program.js';
|
|
3
|
+
import { registerAdminReindexCommand } from './admin-reindex.js';
|
|
3
4
|
import { registerRuntimeCommands } from './commands/runtime-commands.js';
|
|
4
5
|
import { profileMark } from './startup-profile.js';
|
|
5
|
-
profileMark('module:
|
|
6
|
-
export function
|
|
7
|
-
const
|
|
8
|
-
.command('
|
|
9
|
-
.description('Low-level project initialization and
|
|
6
|
+
profileMark('module:admin');
|
|
7
|
+
export function registerAdminCommands(program, context) {
|
|
8
|
+
const admin = program
|
|
9
|
+
.command('admin')
|
|
10
|
+
.description('Low-level project initialization, runtime, and index management')
|
|
10
11
|
.showHelpAfterError();
|
|
11
|
-
|
|
12
|
-
context.writeDebug?.('
|
|
12
|
+
admin.hook('preAction', (_thisCommand, actionCommand) => {
|
|
13
|
+
context.writeDebug?.('admin', actionCommand);
|
|
13
14
|
});
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
admin.action(() => {
|
|
16
|
+
admin.outputHelp();
|
|
16
17
|
context.setExitCode(0);
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
+
admin
|
|
19
20
|
.command('init')
|
|
20
21
|
.description('Initialize a Git-backed KTX project directory for maintenance scripts')
|
|
21
22
|
.argument('[directory]', 'Project directory')
|
|
@@ -26,7 +27,7 @@ export function registerDevCommands(program, context) {
|
|
|
26
27
|
force: commandOptions.force === true,
|
|
27
28
|
}, context.io));
|
|
28
29
|
});
|
|
29
|
-
|
|
30
|
+
admin
|
|
30
31
|
.command('schema')
|
|
31
32
|
.description('Print a JSON Schema describing ktx.yaml (for editors and LLM agents)')
|
|
32
33
|
.option('--output <file>', 'Write the schema to a file instead of stdout')
|
|
@@ -44,5 +45,6 @@ export function registerDevCommands(program, context) {
|
|
|
44
45
|
}
|
|
45
46
|
context.setExitCode(0);
|
|
46
47
|
});
|
|
47
|
-
registerRuntimeCommands(
|
|
48
|
+
registerRuntimeCommands(admin, context);
|
|
49
|
+
registerAdminReindexCommand(admin, context);
|
|
48
50
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,12 +20,12 @@ function makeIo() {
|
|
|
20
20
|
stderr: () => stderr,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
describe('
|
|
24
|
-
it('prints visible
|
|
23
|
+
describe('admin Commander tree', () => {
|
|
24
|
+
it('prints visible admin help with supported low-level command groups', async () => {
|
|
25
25
|
const testIo = makeIo();
|
|
26
|
-
await expect(runKtxCli(['
|
|
27
|
-
expect(testIo.stdout()).toContain('Usage: ktx
|
|
28
|
-
for (const command of ['init', 'runtime']) {
|
|
26
|
+
await expect(runKtxCli(['admin', '--help'], testIo.io)).resolves.toBe(0);
|
|
27
|
+
expect(testIo.stdout()).toContain('Usage: ktx admin [options] [command]');
|
|
28
|
+
for (const command of ['init', 'runtime', 'reindex']) {
|
|
29
29
|
expect(testIo.stdout()).toContain(command);
|
|
30
30
|
}
|
|
31
31
|
for (const removed of [
|
|
@@ -47,23 +47,28 @@ describe('dev Commander tree', () => {
|
|
|
47
47
|
}
|
|
48
48
|
expect(testIo.stderr()).toBe('');
|
|
49
49
|
});
|
|
50
|
-
it('lists
|
|
50
|
+
it('lists admin in root command rows', async () => {
|
|
51
51
|
const testIo = makeIo();
|
|
52
52
|
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
|
|
53
53
|
expect(testIo.stdout()).not.toContain('Advanced:');
|
|
54
|
-
expect(testIo.stdout()).toContain('
|
|
55
|
-
expect(testIo.stdout()).toMatch(/Low-level project initialization
|
|
54
|
+
expect(testIo.stdout()).toContain('admin');
|
|
55
|
+
expect(testIo.stdout()).toMatch(/Low-level project initialization,\s+runtime,\s+and index management/);
|
|
56
56
|
expect(testIo.stderr()).toBe('');
|
|
57
57
|
});
|
|
58
|
-
it('
|
|
58
|
+
it('does not keep a dev alias', async () => {
|
|
59
|
+
const testIo = makeIo();
|
|
60
|
+
await expect(runKtxCli(['dev', '--help'], testIo.io)).resolves.toBe(1);
|
|
61
|
+
expect(testIo.stderr()).toContain("unknown command 'dev'");
|
|
62
|
+
});
|
|
63
|
+
it('keeps project scaffolding under admin init', async () => {
|
|
59
64
|
const { mkdtemp, readFile, rm } = await import('node:fs/promises');
|
|
60
65
|
const { tmpdir } = await import('node:os');
|
|
61
66
|
const { join } = await import('node:path');
|
|
62
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-
|
|
67
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-'));
|
|
63
68
|
const projectDir = join(tempDir, 'warehouse');
|
|
64
69
|
const testIo = makeIo();
|
|
65
70
|
try {
|
|
66
|
-
await expect(runKtxCli(['
|
|
71
|
+
await expect(runKtxCli(['admin', 'init', projectDir], testIo.io)).resolves.toBe(0);
|
|
67
72
|
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
|
68
73
|
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.not.toContain('project:');
|
|
69
74
|
expect(testIo.stderr()).toBe('');
|
|
@@ -72,15 +77,15 @@ describe('dev Commander tree', () => {
|
|
|
72
77
|
await rm(tempDir, { recursive: true, force: true });
|
|
73
78
|
}
|
|
74
79
|
});
|
|
75
|
-
it('uses global project-dir for
|
|
80
|
+
it('uses global project-dir for admin init when the positional directory is omitted', async () => {
|
|
76
81
|
const { mkdtemp, rm } = await import('node:fs/promises');
|
|
77
82
|
const { tmpdir } = await import('node:os');
|
|
78
83
|
const { join } = await import('node:path');
|
|
79
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-
|
|
84
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-init-global-'));
|
|
80
85
|
const projectDir = join(tempDir, 'global-init');
|
|
81
86
|
const testIo = makeIo();
|
|
82
87
|
try {
|
|
83
|
-
await expect(runKtxCli(['--project-dir', projectDir, '
|
|
88
|
+
await expect(runKtxCli(['--project-dir', projectDir, 'admin', 'init'], testIo.io)).resolves.toBe(0);
|
|
84
89
|
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
|
85
90
|
expect(testIo.stderr()).toBe('');
|
|
86
91
|
}
|
|
@@ -92,13 +97,13 @@ describe('dev Commander tree', () => {
|
|
|
92
97
|
const { mkdtemp, rm } = await import('node:fs/promises');
|
|
93
98
|
const { tmpdir } = await import('node:os');
|
|
94
99
|
const { join } = await import('node:path');
|
|
95
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-
|
|
100
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-admin-schema-'));
|
|
96
101
|
const missingProjectDir = join(tempDir, 'missing-project');
|
|
97
102
|
const originalProjectDir = process.env.KTX_PROJECT_DIR;
|
|
98
103
|
const testIo = makeIo();
|
|
99
104
|
try {
|
|
100
105
|
process.env.KTX_PROJECT_DIR = missingProjectDir;
|
|
101
|
-
await expect(runKtxCli(['
|
|
106
|
+
await expect(runKtxCli(['admin', 'schema'], testIo.io)).resolves.toBe(0);
|
|
102
107
|
expect(JSON.parse(testIo.stdout())).toMatchObject({
|
|
103
108
|
title: 'ktx.yaml',
|
|
104
109
|
type: 'object',
|
|
@@ -115,19 +120,19 @@ describe('dev Commander tree', () => {
|
|
|
115
120
|
await rm(tempDir, { recursive: true, force: true });
|
|
116
121
|
}
|
|
117
122
|
});
|
|
118
|
-
it('rejects removed
|
|
123
|
+
it('rejects removed admin command groups', async () => {
|
|
119
124
|
for (const argv of [
|
|
120
|
-
['
|
|
121
|
-
['
|
|
122
|
-
['
|
|
123
|
-
['
|
|
124
|
-
['
|
|
125
|
-
['
|
|
126
|
-
['
|
|
127
|
-
['
|
|
128
|
-
['
|
|
129
|
-
['
|
|
130
|
-
['
|
|
125
|
+
['admin', 'doctor', 'setup'],
|
|
126
|
+
['admin', 'runtime', 'doctor'],
|
|
127
|
+
['admin', 'runtime', 'prune', '--dry-run'],
|
|
128
|
+
['admin', 'scan', 'warehouse'],
|
|
129
|
+
['admin', 'ingest', 'run'],
|
|
130
|
+
['admin', 'mapping', 'list'],
|
|
131
|
+
['admin', 'completion', 'zsh'],
|
|
132
|
+
['admin', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', ''],
|
|
133
|
+
['admin', 'knowledge', 'list'],
|
|
134
|
+
['admin', 'model', 'list'],
|
|
135
|
+
['admin', 'artifacts'],
|
|
131
136
|
]) {
|
|
132
137
|
const testIo = makeIo();
|
|
133
138
|
await expect(runKtxCli(argv, testIo.io)).resolves.toBe(1);
|
|
@@ -136,8 +141,8 @@ describe('dev Commander tree', () => {
|
|
|
136
141
|
});
|
|
137
142
|
it.each([
|
|
138
143
|
{
|
|
139
|
-
argv: ['
|
|
140
|
-
expected: ['Usage: ktx
|
|
144
|
+
argv: ['admin', 'runtime', '--help'],
|
|
145
|
+
expected: ['Usage: ktx admin runtime', 'install', 'start', 'stop', 'status'],
|
|
141
146
|
},
|
|
142
147
|
])('prints generated nested help for $argv', async ({ argv, expected }) => {
|
|
143
148
|
const io = makeIo();
|
|
@@ -146,7 +151,7 @@ describe('dev Commander tree', () => {
|
|
|
146
151
|
for (const text of expected) {
|
|
147
152
|
expect(io.stdout()).toContain(text);
|
|
148
153
|
}
|
|
149
|
-
if (argv.join(' ') === '
|
|
154
|
+
if (argv.join(' ') === 'admin runtime --help') {
|
|
150
155
|
expect(io.stdout()).not.toContain('prune');
|
|
151
156
|
expect(io.stdout()).not.toContain('doctor');
|
|
152
157
|
}
|
package/dist/cli-program.js
CHANGED
|
@@ -9,14 +9,14 @@ import { registerSetupCommands } from './commands/setup-commands.js';
|
|
|
9
9
|
import { registerSlCommands } from './commands/sl-commands.js';
|
|
10
10
|
import { registerSqlCommands } from './commands/sql-commands.js';
|
|
11
11
|
import { registerStatusCommands } from './commands/status-commands.js';
|
|
12
|
-
import {
|
|
12
|
+
import { registerAdminCommands } from './admin.js';
|
|
13
13
|
import { renderMissingProjectMessage } from './doctor.js';
|
|
14
14
|
import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
|
|
15
15
|
import { profileMark, profileSpan } from './startup-profile.js';
|
|
16
16
|
profileMark('module:cli-program');
|
|
17
17
|
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'sql', 'status', 'mcp']);
|
|
18
|
-
const
|
|
19
|
-
const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx
|
|
18
|
+
const PROJECT_INDEPENDENT_ADMIN_COMMANDS = new Set(['runtime', 'schema']);
|
|
19
|
+
const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx admin init']);
|
|
20
20
|
const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']);
|
|
21
21
|
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
|
22
22
|
const GLOBAL_OPTIONS_WITHOUT_VALUE = new Set(['--debug', '--help', '-h', '--version', '-v']);
|
|
@@ -105,14 +105,14 @@ function isProjectAwareCommand(path) {
|
|
|
105
105
|
return false;
|
|
106
106
|
}
|
|
107
107
|
const rootCommand = path[1];
|
|
108
|
-
if (rootCommand === '
|
|
109
|
-
return path[2] !== undefined && !
|
|
108
|
+
if (rootCommand === 'admin') {
|
|
109
|
+
return path[2] !== undefined && !PROJECT_INDEPENDENT_ADMIN_COMMANDS.has(path[2]);
|
|
110
110
|
}
|
|
111
111
|
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
|
|
112
112
|
}
|
|
113
113
|
function shouldSuppressProjectDirLine(path, options) {
|
|
114
114
|
const commandPathKey = path.join(' ');
|
|
115
|
-
if (commandPathKey === 'ktx
|
|
115
|
+
if (commandPathKey === 'ktx admin init') {
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
118
|
if (commandPathKey === 'ktx setup') {
|
|
@@ -315,7 +315,7 @@ export function buildKtxProgram(options) {
|
|
|
315
315
|
registerSqlCommands(program, context);
|
|
316
316
|
registerStatusCommands(program, context);
|
|
317
317
|
registerMcpCommands(program, context);
|
|
318
|
-
|
|
318
|
+
registerAdminCommands(program, context);
|
|
319
319
|
return program;
|
|
320
320
|
}
|
|
321
321
|
export async function runCommanderKtxCli(argv, io, deps, info, options) {
|
package/dist/cli-program.test.js
CHANGED
|
@@ -25,7 +25,7 @@ describe('buildKtxProgram', () => {
|
|
|
25
25
|
});
|
|
26
26
|
expect(program.name()).toBe('ktx');
|
|
27
27
|
const topLevel = program.commands.map((command) => command.name()).sort();
|
|
28
|
-
for (const expected of ['setup', 'connection', 'ingest', 'sl', '
|
|
28
|
+
for (const expected of ['setup', 'connection', 'ingest', 'sl', 'admin']) {
|
|
29
29
|
expect(topLevel).toContain(expected);
|
|
30
30
|
}
|
|
31
31
|
});
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KtxConnectionArgs } from './connection.js';
|
|
2
|
+
import type { KtxAdminReindexArgs } from './admin-reindex.js';
|
|
2
3
|
import type { KtxDoctorArgs } from './doctor.js';
|
|
3
4
|
import type { KtxKnowledgeArgs } from './knowledge.js';
|
|
4
5
|
import type { KtxPublicIngestArgs } from './public-ingest.js';
|
|
@@ -25,6 +26,7 @@ export interface KtxCliIo {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
export interface KtxCliDeps {
|
|
29
|
+
adminReindex?: (args: KtxAdminReindexArgs, io: KtxCliIo) => Promise<number>;
|
|
28
30
|
setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise<number>;
|
|
29
31
|
connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise<number>;
|
|
30
32
|
doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise<number>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveCommandProjectDir } from '../cli-program.js';
|
|
2
2
|
import { profileMark } from '../startup-profile.js';
|
|
3
|
+
import { resolveConnectionSelection } from './connection-selection.js';
|
|
3
4
|
profileMark('module:commands/connection-commands');
|
|
4
5
|
async function runConnectionArgs(context, args) {
|
|
5
6
|
const runner = context.deps.connection ?? (await import('../connection.js')).runKtxConnection;
|
|
@@ -10,7 +11,10 @@ export function registerConnectionCommands(program, context, commandName = 'conn
|
|
|
10
11
|
.command(commandName)
|
|
11
12
|
.description('List and test configured connections')
|
|
12
13
|
.showHelpAfterError()
|
|
13
|
-
.addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n')
|
|
14
|
+
.addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the nearest ktx.yaml or current working directory.\n')
|
|
15
|
+
.action(async (_options, command) => {
|
|
16
|
+
await runConnectionArgs(context, { command: 'list', projectDir: resolveCommandProjectDir(command) });
|
|
17
|
+
});
|
|
14
18
|
connection.hook('preAction', (_thisCommand, actionCommand) => {
|
|
15
19
|
context.writeDebug?.(commandName, actionCommand);
|
|
16
20
|
});
|
|
@@ -22,25 +26,22 @@ export function registerConnectionCommands(program, context, commandName = 'conn
|
|
|
22
26
|
});
|
|
23
27
|
connection
|
|
24
28
|
.command('test')
|
|
25
|
-
.description('Test
|
|
26
|
-
.argument('[connectionId]', 'KTX connection id (omit
|
|
29
|
+
.description('Test one or all configured connections (default: all)')
|
|
30
|
+
.argument('[connectionId]', 'KTX connection id to test (omit to test all)')
|
|
27
31
|
.option('--all', 'Test every configured connection and print a summary list')
|
|
28
32
|
.action(async (connectionId, options, command) => {
|
|
29
|
-
|
|
30
|
-
if (all && connectionId !== undefined) {
|
|
33
|
+
if (options.all === true && connectionId !== undefined) {
|
|
31
34
|
command.error('error: --all cannot be combined with a connection id argument');
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (all) {
|
|
36
|
+
const selection = resolveConnectionSelection({ connectionId, all: options.all === true });
|
|
37
|
+
if (selection.kind === 'all') {
|
|
37
38
|
await runConnectionArgs(context, { command: 'test-all', projectDir: resolveCommandProjectDir(command) });
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
await runConnectionArgs(context, {
|
|
41
42
|
command: 'test',
|
|
42
43
|
projectDir: resolveCommandProjectDir(command),
|
|
43
|
-
connectionId: connectionId,
|
|
44
|
+
connectionId: selection.connectionId,
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
47
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ConnectionSelection = {
|
|
2
|
+
kind: 'all';
|
|
3
|
+
} | {
|
|
4
|
+
kind: 'single';
|
|
5
|
+
connectionId: string;
|
|
6
|
+
};
|
|
7
|
+
export interface ResolveConnectionSelectionInput {
|
|
8
|
+
connectionId?: string | undefined;
|
|
9
|
+
all: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveConnectionSelection(input: ResolveConnectionSelectionInput): ConnectionSelection;
|