@kaelio/ktx 0.10.0 → 0.11.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.10.0-py3-none-any.whl → kaelio_ktx-0.11.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +3 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-program.js +4 -0
- package/dist/cli-runtime.d.ts +2 -0
- package/dist/cli-runtime.js +12 -6
- package/dist/community-cta.d.ts +11 -0
- package/dist/community-cta.js +19 -0
- package/dist/context/core/git-env.d.ts +12 -1
- package/dist/context/core/git-env.js +17 -2
- package/dist/context/core/git.service.js +15 -7
- package/dist/io/tty.d.ts +9 -0
- package/dist/io/tty.js +5 -0
- package/dist/links.d.ts +1 -0
- package/dist/links.js +1 -0
- package/dist/setup-agents.js +1 -5
- package/dist/setup-prompts.js +1 -5
- package/dist/setup.d.ts +25 -0
- package/dist/setup.js +77 -6
- package/dist/telemetry/command-hook.d.ts +24 -0
- package/dist/telemetry/command-hook.js +37 -3
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.js +2 -2
- package/package.json +1 -1
package/dist/cli-program.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Command, InvalidArgumentError } from '@commander-js/extra-typings';
|
|
4
|
+
import { SLACK_HELP_FOOTER, writeErrorCommunityHint } from './community-cta.js';
|
|
4
5
|
import { registerCompletionCommands } from './commands/completion-commands.js';
|
|
5
6
|
import { registerConnectionCommands } from './commands/connection-commands.js';
|
|
6
7
|
import { registerIngestCommands } from './commands/ingest-commands.js';
|
|
@@ -168,6 +169,7 @@ function createBaseProgram(info, io) {
|
|
|
168
169
|
.helpOption('-h, --help', 'Show this help text')
|
|
169
170
|
.configureHelp({ showGlobalOptions: true })
|
|
170
171
|
.showHelpAfterError()
|
|
172
|
+
.addHelpText('after', `\n${SLACK_HELP_FOOTER}`)
|
|
171
173
|
.exitOverride()
|
|
172
174
|
.configureOutput({
|
|
173
175
|
writeOut: (chunk) => io.stdout.write(chunk),
|
|
@@ -433,6 +435,7 @@ export async function runCommanderKtxCli(argv, io, deps, info, options) {
|
|
|
433
435
|
io,
|
|
434
436
|
});
|
|
435
437
|
io.stderr.write(`${formatCliError(error)}\n`);
|
|
438
|
+
writeErrorCommunityHint(io, 'error');
|
|
436
439
|
return 1;
|
|
437
440
|
}
|
|
438
441
|
}
|
|
@@ -458,6 +461,7 @@ export async function runCommanderKtxCli(argv, io, deps, info, options) {
|
|
|
458
461
|
}
|
|
459
462
|
else {
|
|
460
463
|
io.stderr.write(`${formatCliError(error)}\n`);
|
|
464
|
+
writeErrorCommunityHint(io, 'error');
|
|
461
465
|
exitCode = 1;
|
|
462
466
|
}
|
|
463
467
|
}
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -49,5 +49,7 @@ export declare function runInitForCommander(args: {
|
|
|
49
49
|
}, io: KtxCliIo): Promise<number>;
|
|
50
50
|
/** @internal */
|
|
51
51
|
export declare function createGlobalExceptionReporter(io: KtxCliIo, info: KtxCliPackageInfo): (source: "uncaughtException" | "unhandledRejection", error: unknown) => Promise<void>;
|
|
52
|
+
/** @internal */
|
|
53
|
+
export declare function writeGlobalExceptionToStderr(io: KtxCliIo, error: unknown): void;
|
|
52
54
|
export declare function installGlobalExceptionHandlers(io: KtxCliIo, info: KtxCliPackageInfo): () => void;
|
|
53
55
|
export declare function runKtxCli(argv?: string[], io?: KtxCliIo, deps?: KtxCliDeps): Promise<number>;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { profileMark, profileSpan } from './startup-profile.js';
|
|
3
3
|
import { assertCliVersion } from './release-version.js';
|
|
4
|
+
import { writeErrorCommunityHint } from './community-cta.js';
|
|
4
5
|
profileMark('module:cli-runtime');
|
|
5
6
|
const requirePackageJson = createRequire(import.meta.url);
|
|
6
7
|
export function getKtxCliPackageInfo() {
|
|
@@ -88,6 +89,16 @@ export function createGlobalExceptionReporter(io, info) {
|
|
|
88
89
|
await shutdownTelemetryEmitter();
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
/** @internal */
|
|
93
|
+
export function writeGlobalExceptionToStderr(io, error) {
|
|
94
|
+
if (error instanceof Error && error.stack) {
|
|
95
|
+
io.stderr.write(`${error.stack}\n`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
io.stderr.write(`${String(error)}\n`);
|
|
99
|
+
}
|
|
100
|
+
writeErrorCommunityHint(io, 'crash');
|
|
101
|
+
}
|
|
91
102
|
export function installGlobalExceptionHandlers(io, info) {
|
|
92
103
|
const report = createGlobalExceptionReporter(io, info);
|
|
93
104
|
const handle = (source, error) => {
|
|
@@ -98,12 +109,7 @@ export function installGlobalExceptionHandlers(io, info) {
|
|
|
98
109
|
catch {
|
|
99
110
|
// Best-effort: preserve Node's process termination behavior.
|
|
100
111
|
}
|
|
101
|
-
|
|
102
|
-
io.stderr.write(`${error.stack}\n`);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
io.stderr.write(`${String(error)}\n`);
|
|
106
|
-
}
|
|
112
|
+
writeGlobalExceptionToStderr(io, error);
|
|
107
113
|
process.exit(1);
|
|
108
114
|
})();
|
|
109
115
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { KtxCliIo } from './cli-runtime.js';
|
|
2
|
+
type ErrorCtaVariant = 'error' | 'crash';
|
|
3
|
+
/** @internal */
|
|
4
|
+
export declare const SLACK_HELP_FOOTER = "Community & support: https://ktx.sh/slack";
|
|
5
|
+
/** @internal */
|
|
6
|
+
export declare const SLACK_SETUP_NOTE: {
|
|
7
|
+
readonly title: "Community";
|
|
8
|
+
readonly body: "Questions or feedback? Join the ktx Slack: https://ktx.sh/slack";
|
|
9
|
+
};
|
|
10
|
+
export declare function writeErrorCommunityHint(io: KtxCliIo, variant: ErrorCtaVariant): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isWritableTtyOutput } from './io/tty.js';
|
|
2
|
+
import { dim } from './io/symbols.js';
|
|
3
|
+
import { SLACK_URL } from './links.js';
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const SLACK_HELP_FOOTER = `Community & support: ${SLACK_URL}`;
|
|
6
|
+
/** @internal */
|
|
7
|
+
export const SLACK_SETUP_NOTE = {
|
|
8
|
+
title: 'Community',
|
|
9
|
+
body: `Questions or feedback? Join the ktx Slack: ${SLACK_URL}`,
|
|
10
|
+
};
|
|
11
|
+
export function writeErrorCommunityHint(io, variant) {
|
|
12
|
+
if (!isWritableTtyOutput(io.stderr)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const line = variant === 'crash'
|
|
16
|
+
? `This may be a bug - report it or ask in the ktx community: ${SLACK_URL}`
|
|
17
|
+
: `Stuck? The ktx community can help: ${SLACK_URL}`;
|
|
18
|
+
io.stderr.write(`${dim(line)}\n`);
|
|
19
|
+
}
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import { type SimpleGit } from 'simple-git';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Create a simple-git client scoped to `baseDir`. When an identity is provided, ktx's own
|
|
4
|
+
* commits carry it through the GIT_AUTHOR and GIT_COMMITTER environment variables instead of
|
|
5
|
+
* relying on repo-local or global git config. This keeps commits working when the project
|
|
6
|
+
* directory is an existing repo ktx did not create and the machine has no configured git
|
|
7
|
+
* identity (e.g. a fresh Mac with no ~/.gitconfig), without mutating the user's repo config.
|
|
8
|
+
* Explicit `--author` flags on individual commits still take precedence over GIT_AUTHOR_NAME.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createSimpleGit(baseDir: string, identity?: {
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
}): SimpleGit;
|
|
@@ -21,6 +21,21 @@ function sanitizedGitEnv(env = process.env) {
|
|
|
21
21
|
}
|
|
22
22
|
return sanitized;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Create a simple-git client scoped to `baseDir`. When an identity is provided, ktx's own
|
|
26
|
+
* commits carry it through the GIT_AUTHOR and GIT_COMMITTER environment variables instead of
|
|
27
|
+
* relying on repo-local or global git config. This keeps commits working when the project
|
|
28
|
+
* directory is an existing repo ktx did not create and the machine has no configured git
|
|
29
|
+
* identity (e.g. a fresh Mac with no ~/.gitconfig), without mutating the user's repo config.
|
|
30
|
+
* Explicit `--author` flags on individual commits still take precedence over GIT_AUTHOR_NAME.
|
|
31
|
+
*/
|
|
32
|
+
export function createSimpleGit(baseDir, identity) {
|
|
33
|
+
const env = sanitizedGitEnv();
|
|
34
|
+
if (identity?.name && identity.email) {
|
|
35
|
+
env.GIT_AUTHOR_NAME = identity.name;
|
|
36
|
+
env.GIT_AUTHOR_EMAIL = identity.email;
|
|
37
|
+
env.GIT_COMMITTER_NAME = identity.name;
|
|
38
|
+
env.GIT_COMMITTER_EMAIL = identity.email;
|
|
39
|
+
}
|
|
40
|
+
return simpleGit({ baseDir, unsafe: { allowUnsafeAskPass: true } }).env(env);
|
|
26
41
|
}
|
|
@@ -47,8 +47,12 @@ export class GitService {
|
|
|
47
47
|
// Ensure config directory exists
|
|
48
48
|
await fs.mkdir(this.configDir, { recursive: true });
|
|
49
49
|
this.logger.log(`Config directory ensured at: ${this.configDir}`);
|
|
50
|
-
// Initialize simple-git
|
|
51
|
-
this
|
|
50
|
+
// Initialize simple-git. Carry ktx's identity in the environment so commits succeed even
|
|
51
|
+
// when this repo already exists and the machine has no configured git identity.
|
|
52
|
+
this.git = createSimpleGit(this.configDir, {
|
|
53
|
+
name: this.config.git.userName,
|
|
54
|
+
email: this.config.git.userEmail,
|
|
55
|
+
});
|
|
52
56
|
// Initialize git repository
|
|
53
57
|
await this.initialize();
|
|
54
58
|
}
|
|
@@ -58,9 +62,6 @@ export class GitService {
|
|
|
58
62
|
const isRepo = await this.git.checkIsRepo();
|
|
59
63
|
if (!isRepo) {
|
|
60
64
|
await this.git.init();
|
|
61
|
-
const gitConfig = this.config.git;
|
|
62
|
-
await this.git.addConfig('user.name', gitConfig.userName);
|
|
63
|
-
await this.git.addConfig('user.email', gitConfig.userEmail);
|
|
64
65
|
this.logger.log('Initialized git repository');
|
|
65
66
|
}
|
|
66
67
|
// Keep any auto-maintenance triggered by writes in-process. Detached maintenance can
|
|
@@ -81,7 +82,11 @@ export class GitService {
|
|
|
81
82
|
}
|
|
82
83
|
catch (error) {
|
|
83
84
|
this.logger.error('Failed to initialize git repository', error);
|
|
84
|
-
|
|
85
|
+
// Preserve the underlying git error: the generic message alone is undiagnosable in
|
|
86
|
+
// telemetry and unactionable for the user. The exception reporter walks `cause` and
|
|
87
|
+
// redacts secrets before send.
|
|
88
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
89
|
+
throw new Error(`Failed to initialize git repository: ${detail}`, { cause: error });
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
async commitFile(filePath, commitMessage, author, authorEmail) {
|
|
@@ -740,7 +745,10 @@ export class GitService {
|
|
|
740
745
|
*/
|
|
741
746
|
forWorktree(workdir) {
|
|
742
747
|
const scoped = new GitService(this.config, this.logger);
|
|
743
|
-
scoped.git = createSimpleGit(workdir
|
|
748
|
+
scoped.git = createSimpleGit(workdir, {
|
|
749
|
+
name: this.config.git.userName,
|
|
750
|
+
email: this.config.git.userEmail,
|
|
751
|
+
});
|
|
744
752
|
scoped.configDir = workdir;
|
|
745
753
|
return scoped;
|
|
746
754
|
}
|
package/dist/io/tty.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import type { KtxCliIo } from '../cli-runtime.js';
|
|
3
|
+
type KtxCliOutput = (KtxCliIo['stdout'] | KtxCliIo['stderr']) & {
|
|
4
|
+
isTTY?: boolean;
|
|
5
|
+
columns?: number;
|
|
6
|
+
on?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare function isWritableTtyOutput(output: KtxCliOutput): output is KtxCliOutput & Writable;
|
|
9
|
+
export {};
|
package/dist/io/tty.js
ADDED
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SLACK_URL = "https://ktx.sh/slack";
|
package/dist/links.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SLACK_URL = 'https://ktx.sh/slack';
|
package/dist/setup-agents.js
CHANGED
|
@@ -9,14 +9,10 @@ import { markKtxSetupStateStepComplete } from './context/project/setup-config.js
|
|
|
9
9
|
import { serializeKtxProjectConfig } from './context/project/config.js';
|
|
10
10
|
import { strToU8, zipSync } from 'fflate';
|
|
11
11
|
import { errorMessage, writePrefixedLines } from './clack.js';
|
|
12
|
+
import { isWritableTtyOutput } from './io/tty.js';
|
|
12
13
|
import { createKtxSetupPromptAdapter, createKtxSetupUiAdapter, } from './setup-prompts.js';
|
|
13
14
|
import { readKtxMcpDaemonStatus } from './managed-mcp-daemon.js';
|
|
14
15
|
const MCP_DAEMON_REQUIRED_NOTICE = 'mcp-daemon-required';
|
|
15
|
-
function isWritableTtyOutput(output) {
|
|
16
|
-
return (output.isTTY === true &&
|
|
17
|
-
typeof output.on === 'function' &&
|
|
18
|
-
typeof output.columns !== 'undefined');
|
|
19
|
-
}
|
|
20
16
|
function writeSetupInfo(io, message) {
|
|
21
17
|
if (isWritableTtyOutput(io.stdout)) {
|
|
22
18
|
log.info(message, { output: io.stdout });
|
package/dist/setup-prompts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { autocomplete, autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
|
|
2
|
+
import { isWritableTtyOutput } from './io/tty.js';
|
|
2
3
|
import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
|
|
3
4
|
import { revealPassword } from './reveal-password-prompt.js';
|
|
4
5
|
import { withSetupInterruptConfirmation } from './setup-interrupt.js';
|
|
@@ -101,11 +102,6 @@ export function createKtxSetupPromptAdapter(options) {
|
|
|
101
102
|
},
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
|
-
function isWritableTtyOutput(output) {
|
|
105
|
-
return (output.isTTY === true &&
|
|
106
|
-
typeof output.on === 'function' &&
|
|
107
|
-
typeof output.columns !== 'undefined');
|
|
108
|
-
}
|
|
109
105
|
export function createKtxSetupUiAdapter() {
|
|
110
106
|
return {
|
|
111
107
|
intro(title, io) {
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type KtxCliIo } from './cli-runtime.js';
|
|
2
2
|
import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
|
|
3
|
+
import type { CommandOutcome } from './telemetry/index.js';
|
|
3
4
|
import { type KtxAgentScope, type KtxAgentTarget, type KtxSetupAgentsDeps, runKtxSetupAgentsStep } from './setup-agents.js';
|
|
4
5
|
import { type KtxSetupDatabaseDriver, type KtxSetupDatabasesDeps, runKtxSetupDatabasesStep } from './setup-databases.js';
|
|
5
6
|
import { type KtxSetupEmbeddingsDeps, runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
|
|
@@ -125,6 +126,7 @@ export interface KtxSetupDeps {
|
|
|
125
126
|
entryMenuDeps?: KtxSetupEntryMenuDeps;
|
|
126
127
|
setupUi?: KtxSetupUiAdapter;
|
|
127
128
|
}
|
|
129
|
+
type TelemetrySetupStep = 'project' | 'runtime' | 'models' | 'embeddings' | 'databases' | 'sources' | 'context' | 'agents' | 'demo-tour';
|
|
128
130
|
export interface KtxSetupEntryMenuPromptAdapter {
|
|
129
131
|
select(options: {
|
|
130
132
|
message: string;
|
|
@@ -135,6 +137,28 @@ export interface KtxSetupEntryMenuPromptAdapter {
|
|
|
135
137
|
export interface KtxSetupEntryMenuDeps {
|
|
136
138
|
prompts?: KtxSetupEntryMenuPromptAdapter;
|
|
137
139
|
}
|
|
140
|
+
interface SetupCommandAnnotation {
|
|
141
|
+
outcome: CommandOutcome;
|
|
142
|
+
errorClass?: string;
|
|
143
|
+
errorDetail?: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Single source of truth for how a non-ready setup step ends: the process exit
|
|
147
|
+
* code and the telemetry annotation are both derived from one classification,
|
|
148
|
+
* so they can never disagree. A genuine failure (`error`) exits non-zero; an
|
|
149
|
+
* abort — the user leaving an interactive wizard — exits 0, matching the entry
|
|
150
|
+
* menu's "Exit", a project cancellation, and a confirmed Ctrl+C.
|
|
151
|
+
*/
|
|
152
|
+
/** @internal */
|
|
153
|
+
export declare function setupTerminalOutcome(input: {
|
|
154
|
+
status: 'failed' | 'missing-input' | 'cancelled';
|
|
155
|
+
step: TelemetrySetupStep;
|
|
156
|
+
interactive: boolean;
|
|
157
|
+
errorDetail?: string;
|
|
158
|
+
}): {
|
|
159
|
+
exitCode: number;
|
|
160
|
+
annotation: SetupCommandAnnotation;
|
|
161
|
+
};
|
|
138
162
|
export interface ReadKtxSetupStatusOptions {
|
|
139
163
|
cliVersion?: string;
|
|
140
164
|
env?: NodeJS.ProcessEnv;
|
|
@@ -146,3 +170,4 @@ export declare function formatKtxSetupCompletionSummary(status: KtxSetupStatus,
|
|
|
146
170
|
agentNextActions?: string;
|
|
147
171
|
}): string;
|
|
148
172
|
export declare function runKtxSetup(args: KtxSetupArgs, io: KtxCliIo, deps?: KtxSetupDeps): Promise<number>;
|
|
173
|
+
export {};
|
package/dist/setup.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
|
|
|
6
6
|
import { loadKtxProject } from './context/project/project.js';
|
|
7
7
|
import { readKtxSetupState } from './context/project/setup-config.js';
|
|
8
8
|
import { getKtxCliPackageInfo } from './cli-runtime.js';
|
|
9
|
+
import { SLACK_SETUP_NOTE } from './community-cta.js';
|
|
9
10
|
import { formatNextStepLines, formatSetupNextStepLines } from './next-steps.js';
|
|
10
11
|
import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
|
|
11
12
|
import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
|
|
@@ -36,6 +37,61 @@ function setupTelemetryOutcome(status) {
|
|
|
36
37
|
return 'skipped';
|
|
37
38
|
return 'abandoned';
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Classify a terminal non-ready setup status into the `command` telemetry
|
|
42
|
+
* outcome. The setup flow is the decision-maker and knows the difference:
|
|
43
|
+
* - `failed` is a genuine error; attach a step-scoped reason so the dashboard
|
|
44
|
+
* shows an actionable signature instead of a blank.
|
|
45
|
+
* - `missing-input` from a *non-interactive* run is an automation error
|
|
46
|
+
* (required flags absent and no prompt was possible); attach a reason too.
|
|
47
|
+
* - `missing-input` from an interactive prompt, or a project `cancelled`, is the
|
|
48
|
+
* user backing out of the wizard — an abort, not a failure. Keep it out of
|
|
49
|
+
* error telemetry so it stops inflating the error count.
|
|
50
|
+
*
|
|
51
|
+
* `interactive` must reflect whether a prompt could actually be shown — input
|
|
52
|
+
* is enabled AND a TTY is attached. `inputMode: 'auto'` alone is not enough: a
|
|
53
|
+
* piped/CI run without `--no-input` is still non-interactive, and steps such as
|
|
54
|
+
* the project step return `missing-input` ("pass --yes …") there without ever
|
|
55
|
+
* prompting. Treating that as an abort would make a broken automation run exit
|
|
56
|
+
* 0, so it must classify as an error.
|
|
57
|
+
*
|
|
58
|
+
* Reasons are synthetic, step-scoped strings (no user input), so they satisfy
|
|
59
|
+
* the telemetry privacy rules. The step's own `errorDetail`, when present, has
|
|
60
|
+
* already been vetted for the `setup_step` event and is safe to reuse.
|
|
61
|
+
*/
|
|
62
|
+
function setupCommandOutcomeAnnotation(input) {
|
|
63
|
+
if (input.status === 'failed') {
|
|
64
|
+
return {
|
|
65
|
+
outcome: 'error',
|
|
66
|
+
errorClass: 'KtxSetupStepFailed',
|
|
67
|
+
errorDetail: input.errorDetail ?? `${input.step} setup step failed`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (input.status === 'missing-input' && !input.interactive) {
|
|
71
|
+
return {
|
|
72
|
+
outcome: 'error',
|
|
73
|
+
errorClass: 'KtxSetupMissingInput',
|
|
74
|
+
errorDetail: `${input.step} setup step requires input not provided in a non-interactive run`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { outcome: 'aborted' };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Single source of truth for how a non-ready setup step ends: the process exit
|
|
81
|
+
* code and the telemetry annotation are both derived from one classification,
|
|
82
|
+
* so they can never disagree. A genuine failure (`error`) exits non-zero; an
|
|
83
|
+
* abort — the user leaving an interactive wizard — exits 0, matching the entry
|
|
84
|
+
* menu's "Exit", a project cancellation, and a confirmed Ctrl+C.
|
|
85
|
+
*/
|
|
86
|
+
/** @internal */
|
|
87
|
+
export function setupTerminalOutcome(input) {
|
|
88
|
+
const annotation = setupCommandOutcomeAnnotation(input);
|
|
89
|
+
return { exitCode: annotation.outcome === 'error' ? 1 : 0, annotation };
|
|
90
|
+
}
|
|
91
|
+
async function annotateSetupCommandOutcome(annotation) {
|
|
92
|
+
const { annotateCommandOutcome } = await import('./telemetry/index.js');
|
|
93
|
+
annotateCommandOutcome(annotation);
|
|
94
|
+
}
|
|
39
95
|
async function recordSetupStep(input) {
|
|
40
96
|
const { emitTelemetryEvent } = await import('./telemetry/index.js');
|
|
41
97
|
await emitTelemetryEvent({
|
|
@@ -325,6 +381,10 @@ async function runKtxSetupInner(args, io, deps = {}) {
|
|
|
325
381
|
args.inputMode !== 'disabled' &&
|
|
326
382
|
!args.agents &&
|
|
327
383
|
(io.stdout.isTTY === true || deps.entryMenuDeps?.prompts !== undefined);
|
|
384
|
+
// A prompt is only possible when input is enabled AND a TTY is attached. A
|
|
385
|
+
// piped/CI `ktx setup` without `--no-input` is still `inputMode: 'auto'` but
|
|
386
|
+
// cannot prompt, so its `missing-input` is an automation error, not an abort.
|
|
387
|
+
const interactive = args.inputMode !== 'disabled' && io.stdout.isTTY === true;
|
|
328
388
|
setupLoop: while (true) {
|
|
329
389
|
entryAction = undefined;
|
|
330
390
|
if (canShowEntryMenu) {
|
|
@@ -363,7 +423,13 @@ async function runKtxSetupInner(args, io, deps = {}) {
|
|
|
363
423
|
continue;
|
|
364
424
|
}
|
|
365
425
|
if (projectResult.status !== 'ready') {
|
|
366
|
-
|
|
426
|
+
const terminal = setupTerminalOutcome({
|
|
427
|
+
status: projectResult.status,
|
|
428
|
+
step: 'project',
|
|
429
|
+
interactive,
|
|
430
|
+
});
|
|
431
|
+
await annotateSetupCommandOutcome(terminal.annotation);
|
|
432
|
+
return terminal.exitCode;
|
|
367
433
|
}
|
|
368
434
|
const agentsRequested = args.agents || entryAction === 'agents';
|
|
369
435
|
const currentStatus = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
|
|
@@ -576,11 +642,15 @@ async function runKtxSetupInner(args, io, deps = {}) {
|
|
|
576
642
|
cliVersion: args.cliVersion,
|
|
577
643
|
...(stepResult.errorDetail ? { errorDetail: stepResult.errorDetail } : {}),
|
|
578
644
|
});
|
|
579
|
-
if (stepResult.status === 'failed') {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
645
|
+
if (stepResult.status === 'failed' || stepResult.status === 'missing-input') {
|
|
646
|
+
const terminal = setupTerminalOutcome({
|
|
647
|
+
status: stepResult.status,
|
|
648
|
+
step,
|
|
649
|
+
interactive,
|
|
650
|
+
...(stepResult.errorDetail ? { errorDetail: stepResult.errorDetail } : {}),
|
|
651
|
+
});
|
|
652
|
+
await annotateSetupCommandOutcome(terminal.annotation);
|
|
653
|
+
return terminal.exitCode;
|
|
584
654
|
}
|
|
585
655
|
if (stepResult.status === 'back') {
|
|
586
656
|
const previousIndex = previousNavigableStepIndex(stepIndex);
|
|
@@ -630,5 +700,6 @@ async function runKtxSetupInner(args, io, deps = {}) {
|
|
|
630
700
|
}).join('\n'), 'What you can do next', io);
|
|
631
701
|
}
|
|
632
702
|
}
|
|
703
|
+
setupUi.note(SLACK_SETUP_NOTE.body, SLACK_SETUP_NOTE.title, io);
|
|
633
704
|
return 0;
|
|
634
705
|
}
|
|
@@ -6,6 +6,9 @@ interface CommandSpan {
|
|
|
6
6
|
hasProject: boolean;
|
|
7
7
|
attachProjectGroup: boolean;
|
|
8
8
|
startedAt: number;
|
|
9
|
+
annotatedOutcome?: CommandOutcome;
|
|
10
|
+
annotatedErrorClass?: string;
|
|
11
|
+
annotatedErrorDetail?: string;
|
|
9
12
|
}
|
|
10
13
|
export interface CompletedCommandSpan {
|
|
11
14
|
commandPath: string[];
|
|
@@ -19,6 +22,27 @@ export interface CompletedCommandSpan {
|
|
|
19
22
|
projectGroupAttached: boolean;
|
|
20
23
|
}
|
|
21
24
|
export declare function beginCommandSpan(input: CommandSpan): void;
|
|
25
|
+
/**
|
|
26
|
+
* Let a command action record the true outcome and reason on the active span.
|
|
27
|
+
*
|
|
28
|
+
* The Commander wrapper can only derive an outcome from a thrown error or the
|
|
29
|
+
* process exit code, so a command that exits non-zero *without throwing* (e.g.
|
|
30
|
+
* `ktx setup` when the user abandons the wizard) lands as `outcome: 'error'`
|
|
31
|
+
* with no `errorClass`/`errorDetail` — an unactionable blank in the dashboard.
|
|
32
|
+
* The action is the decision-maker: it can mark the run `aborted`, or attach a
|
|
33
|
+
* scrubbed reason so the next occurrence is self-diagnosing. A later thrown
|
|
34
|
+
* error still wins (see {@link completeCommandSpan}), since that is the most
|
|
35
|
+
* authoritative signal and also feeds the `$exception` stream. No-ops when no
|
|
36
|
+
* span is active so call sites stay safe in tests and bare-help paths.
|
|
37
|
+
*
|
|
38
|
+
* Values are emitted verbatim and must already satisfy the telemetry privacy
|
|
39
|
+
* rules — pass synthetic or already-scrubbed strings, never raw user input.
|
|
40
|
+
*/
|
|
41
|
+
export declare function annotateCommandOutcome(input: {
|
|
42
|
+
outcome?: CommandOutcome;
|
|
43
|
+
errorClass?: string;
|
|
44
|
+
errorDetail?: string;
|
|
45
|
+
}): void;
|
|
22
46
|
export declare function completeCommandSpan(input: {
|
|
23
47
|
completedAt: number;
|
|
24
48
|
outcome: CommandOutcome;
|
|
@@ -3,18 +3,52 @@ let activeCommandSpan;
|
|
|
3
3
|
export function beginCommandSpan(input) {
|
|
4
4
|
activeCommandSpan = input;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Let a command action record the true outcome and reason on the active span.
|
|
8
|
+
*
|
|
9
|
+
* The Commander wrapper can only derive an outcome from a thrown error or the
|
|
10
|
+
* process exit code, so a command that exits non-zero *without throwing* (e.g.
|
|
11
|
+
* `ktx setup` when the user abandons the wizard) lands as `outcome: 'error'`
|
|
12
|
+
* with no `errorClass`/`errorDetail` — an unactionable blank in the dashboard.
|
|
13
|
+
* The action is the decision-maker: it can mark the run `aborted`, or attach a
|
|
14
|
+
* scrubbed reason so the next occurrence is self-diagnosing. A later thrown
|
|
15
|
+
* error still wins (see {@link completeCommandSpan}), since that is the most
|
|
16
|
+
* authoritative signal and also feeds the `$exception` stream. No-ops when no
|
|
17
|
+
* span is active so call sites stay safe in tests and bare-help paths.
|
|
18
|
+
*
|
|
19
|
+
* Values are emitted verbatim and must already satisfy the telemetry privacy
|
|
20
|
+
* rules — pass synthetic or already-scrubbed strings, never raw user input.
|
|
21
|
+
*/
|
|
22
|
+
export function annotateCommandOutcome(input) {
|
|
23
|
+
if (!activeCommandSpan) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (input.outcome !== undefined) {
|
|
27
|
+
activeCommandSpan.annotatedOutcome = input.outcome;
|
|
28
|
+
}
|
|
29
|
+
if (input.errorClass !== undefined) {
|
|
30
|
+
activeCommandSpan.annotatedErrorClass = input.errorClass;
|
|
31
|
+
}
|
|
32
|
+
if (input.errorDetail !== undefined) {
|
|
33
|
+
activeCommandSpan.annotatedErrorDetail = input.errorDetail;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
6
36
|
export function completeCommandSpan(input) {
|
|
7
37
|
const span = activeCommandSpan;
|
|
8
38
|
activeCommandSpan = undefined;
|
|
9
39
|
if (!span) {
|
|
10
40
|
return undefined;
|
|
11
41
|
}
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
// Precedence: a thrown error is authoritative; otherwise an action's own
|
|
43
|
+
// annotation; otherwise the wrapper's exit-code-derived outcome.
|
|
44
|
+
const thrown = Boolean(input.error);
|
|
45
|
+
const outcome = thrown ? input.outcome : (span.annotatedOutcome ?? input.outcome);
|
|
46
|
+
const errorClass = thrown ? scrubErrorClass(input.error) : span.annotatedErrorClass;
|
|
47
|
+
const errorDetail = thrown ? formatErrorDetail(input.error) : span.annotatedErrorDetail;
|
|
14
48
|
return {
|
|
15
49
|
commandPath: span.commandPath,
|
|
16
50
|
durationMs: Math.max(0, input.completedAt - span.startedAt),
|
|
17
|
-
outcome
|
|
51
|
+
outcome,
|
|
18
52
|
...(errorClass ? { errorClass } : {}),
|
|
19
53
|
...(errorDetail ? { errorDetail } : {}),
|
|
20
54
|
flagsPresent: span.flagsPresent,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type KtxCliIo, type KtxCliPackageInfo } from '../cli-runtime.js';
|
|
2
|
-
import { beginCommandSpan, completeCommandSpan, type CommandOutcome, type CompletedCommandSpan } from './command-hook.js';
|
|
2
|
+
import { annotateCommandOutcome, beginCommandSpan, completeCommandSpan, type CommandOutcome, type CompletedCommandSpan } from './command-hook.js';
|
|
3
3
|
import { shutdownTelemetryEmitter } from './emitter.js';
|
|
4
4
|
import { reportException, type ExceptionContext } from './exception.js';
|
|
5
5
|
import { type TelemetryCommonEnvelope, type TelemetryEventName, type TelemetryEventProperties } from './events.js';
|
|
6
|
-
export { beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
6
|
+
export { annotateCommandOutcome, beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
7
7
|
export type { CommandOutcome, CompletedCommandSpan, ExceptionContext };
|
|
8
8
|
export declare function showTelemetryNoticeIfNeeded(io: KtxCliIo, packageInfo: KtxCliPackageInfo): Promise<void>;
|
|
9
9
|
type TelemetryEventFields<Name extends TelemetryEventName> = Omit<TelemetryEventProperties<Name>, keyof TelemetryCommonEnvelope>;
|
package/dist/telemetry/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getKtxCliPackageInfo } from '../cli-runtime.js';
|
|
2
2
|
import { loadKtxProject } from '../context/project/project.js';
|
|
3
|
-
import { beginCommandSpan, completeCommandSpan, } from './command-hook.js';
|
|
3
|
+
import { annotateCommandOutcome, beginCommandSpan, completeCommandSpan, } from './command-hook.js';
|
|
4
4
|
import { shutdownTelemetryEmitter, trackTelemetryEvent } from './emitter.js';
|
|
5
5
|
import { reportException } from './exception.js';
|
|
6
6
|
import { buildCommonEnvelope, buildTelemetryEvent, } from './events.js';
|
|
7
7
|
import { computeTelemetryProjectId, loadTelemetryIdentity } from './identity.js';
|
|
8
8
|
import { buildProjectStackSnapshotFields } from './project-snapshot.js';
|
|
9
|
-
export { beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
9
|
+
export { annotateCommandOutcome, beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
10
10
|
export async function showTelemetryNoticeIfNeeded(io, packageInfo) {
|
|
11
11
|
const identity = await loadTelemetryIdentity({
|
|
12
12
|
stderr: io.stderr,
|