@pungoyal/kite-cli 0.3.0 → 0.4.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/README.md +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +74 -5
- package/dist/commands/completion.d.ts +35 -0
- package/dist/commands/completion.js +240 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +190 -0
- package/dist/commands/register.d.ts +25 -0
- package/dist/commands/register.js +75 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.js +39 -0
- package/dist/core/client.js +8 -1
- package/dist/run.js +7 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Then:
|
|
|
50
50
|
kite login
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. Your API secret goes into your OS keyring; the daily access token is stored alongside it.
|
|
53
|
+
This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. The login URL is also printed to the terminal — press `c` while it's waiting to copy it to your clipboard (handy if the browser didn't open, or you want to log in on another device). Your API secret goes into your OS keyring; the daily access token is stored alongside it.
|
|
54
54
|
|
|
55
55
|
**Want to try it without a subscription or real money?** Zerodha runs a public sandbox:
|
|
56
56
|
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
1
2
|
import type { CommandFactory } from './types.js';
|
|
2
3
|
export declare const authCommands: CommandFactory;
|
|
4
|
+
/**
|
|
5
|
+
* While waiting for the browser callback, listen for a `c` keypress to copy the
|
|
6
|
+
* login URL, and for Ctrl-C to abort. Entering raw mode makes us responsible for
|
|
7
|
+
* both restoring the terminal and re-handling Ctrl-C (raw mode no longer raises
|
|
8
|
+
* SIGINT). Returns a no-op when stdin is not a TTY, and an idempotent cleanup
|
|
9
|
+
* that both the caller and the Ctrl-C path can safely call.
|
|
10
|
+
*/
|
|
11
|
+
export declare function listenForKeys(ctx: Context, loginUrl: string, onInterrupt: () => void): () => void;
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCancel, note, password, text } from '@clack/prompts';
|
|
2
|
-
import { buildLoginUrl, computeChecksum, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
|
|
2
|
+
import { buildLoginUrl, computeChecksum, copyToClipboard, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
|
|
3
3
|
import { loadConfig, SANDBOX_CREDENTIALS, saveConfig } from '../core/config.js';
|
|
4
4
|
import { deleteAllSecrets, getSecret, keyringAvailable, setSecret } from '../core/credentials.js';
|
|
5
5
|
import { AbortedError, ExitCode, KiteCliError } from '../core/errors.js';
|
|
@@ -145,23 +145,92 @@ async function callbackFlow(ctx, loginUrl) {
|
|
|
145
145
|
io.info(`Your Kite app's redirect URL must be exactly: ${io.bold(redirectUrl)}`);
|
|
146
146
|
io.info('Set it at https://developers.kite.trade if login fails.');
|
|
147
147
|
io.note('');
|
|
148
|
+
// Show the URL unconditionally: the browser may not have opened, may have
|
|
149
|
+
// landed in the wrong profile, or the user may want to log in on another
|
|
150
|
+
// device. The URL carries only the api_key and CSRF state — no token.
|
|
151
|
+
io.info('Login URL:');
|
|
152
|
+
io.note(` ${loginUrl}`);
|
|
153
|
+
io.note('');
|
|
148
154
|
const opened = await openBrowser(loginUrl);
|
|
149
155
|
if (opened) {
|
|
150
156
|
io.info('Opened your browser to complete login…');
|
|
151
157
|
}
|
|
152
158
|
else {
|
|
153
|
-
io.warn('Could not open a browser automatically. Open
|
|
154
|
-
io.note(` ${loginUrl}`);
|
|
159
|
+
io.warn('Could not open a browser automatically. Open the URL above manually.');
|
|
155
160
|
}
|
|
156
|
-
|
|
161
|
+
// Let the user grab the URL without selecting it in the terminal. Copy-on-`c`
|
|
162
|
+
// and Ctrl-C both funnel through `interrupted`, which loses the race against a
|
|
163
|
+
// successful callback. A single Ctrl-C aborts the wait — raw mode swallows the
|
|
164
|
+
// SIGINT, and the loopback promise never observes ctx.signal on its own.
|
|
165
|
+
let interrupt = () => { };
|
|
166
|
+
const interrupted = new Promise((_, reject) => {
|
|
167
|
+
interrupt = () => reject(new AbortedError('Interrupted.'));
|
|
168
|
+
});
|
|
169
|
+
const onAbort = () => interrupt();
|
|
170
|
+
if (ctx.signal.aborted)
|
|
171
|
+
onAbort();
|
|
172
|
+
else
|
|
173
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
174
|
+
const stopKeys = listenForKeys(ctx, loginUrl, interrupt);
|
|
175
|
+
io.info(`Waiting for the callback (press ${io.bold('c')} to copy the login URL, Ctrl-C to abort)…`);
|
|
157
176
|
try {
|
|
158
|
-
const result = await server.promise;
|
|
177
|
+
const result = await Promise.race([server.promise, interrupted]);
|
|
159
178
|
return result.requestToken;
|
|
160
179
|
}
|
|
161
180
|
finally {
|
|
181
|
+
// Always restore the terminal and detach listeners, even on abort — the
|
|
182
|
+
// loopback promise may never settle, so this cannot hang off it.
|
|
183
|
+
stopKeys();
|
|
184
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
162
185
|
server.close();
|
|
163
186
|
}
|
|
164
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* While waiting for the browser callback, listen for a `c` keypress to copy the
|
|
190
|
+
* login URL, and for Ctrl-C to abort. Entering raw mode makes us responsible for
|
|
191
|
+
* both restoring the terminal and re-handling Ctrl-C (raw mode no longer raises
|
|
192
|
+
* SIGINT). Returns a no-op when stdin is not a TTY, and an idempotent cleanup
|
|
193
|
+
* that both the caller and the Ctrl-C path can safely call.
|
|
194
|
+
*/
|
|
195
|
+
// Exported for tests: the raw-mode lifecycle (enter, restore, detach) and the
|
|
196
|
+
// Ctrl-C byte path are the risky parts, and a real TTY can't be driven in CI.
|
|
197
|
+
export function listenForKeys(ctx, loginUrl, onInterrupt) {
|
|
198
|
+
const { io } = ctx;
|
|
199
|
+
const stdin = process.stdin;
|
|
200
|
+
if (!stdin.isTTY)
|
|
201
|
+
return () => { };
|
|
202
|
+
const wasRaw = stdin.isRaw;
|
|
203
|
+
let stopped = false;
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
if (stopped)
|
|
206
|
+
return;
|
|
207
|
+
stopped = true;
|
|
208
|
+
stdin.off('data', onData);
|
|
209
|
+
stdin.setRawMode(wasRaw);
|
|
210
|
+
stdin.pause();
|
|
211
|
+
};
|
|
212
|
+
const onData = (chunk) => {
|
|
213
|
+
// 0x03 is Ctrl-C, which raw mode delivers as a byte instead of a SIGINT.
|
|
214
|
+
if (chunk.length === 1 && chunk[0] === 0x03) {
|
|
215
|
+
cleanup();
|
|
216
|
+
onInterrupt();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const key = chunk.toString('utf8');
|
|
220
|
+
if (key === 'c' || key === 'C') {
|
|
221
|
+
void copyToClipboard(loginUrl).then((ok) => {
|
|
222
|
+
if (ok)
|
|
223
|
+
io.success('Login URL copied to clipboard.');
|
|
224
|
+
else
|
|
225
|
+
io.warn('Could not copy to the clipboard (no clipboard tool found).');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
stdin.setRawMode(true);
|
|
230
|
+
stdin.resume();
|
|
231
|
+
stdin.on('data', onData);
|
|
232
|
+
return cleanup;
|
|
233
|
+
}
|
|
165
234
|
/** Fallback for remote shells, where no browser can reach 127.0.0.1. */
|
|
166
235
|
async function manualFlow(ctx, loginUrl) {
|
|
167
236
|
const { io } = ctx;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CommandFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shell completion scripts, generated from the live command tree.
|
|
4
|
+
*
|
|
5
|
+
* Rather than hand-maintain a completion file per shell — the exact docs-rot
|
|
6
|
+
* trap a CLI that places real orders cannot afford — this walks the same
|
|
7
|
+
* registration path run() uses (with a no-op runner) and emits command names,
|
|
8
|
+
* subcommand names, and long flags straight from it. A new command or flag is
|
|
9
|
+
* completable as soon as it is added; the only user step is to regenerate after
|
|
10
|
+
* upgrading, the same contract gh, rustup, and kubectl ship.
|
|
11
|
+
*
|
|
12
|
+
* Depth is intentionally capped at command → subcommand + flags, which covers
|
|
13
|
+
* every path this CLI has (`config set`, `margins order`, `orders place`).
|
|
14
|
+
*/
|
|
15
|
+
declare const SHELLS: readonly ['bash', 'zsh', 'fish'];
|
|
16
|
+
type Shell = (typeof SHELLS)[number];
|
|
17
|
+
export declare const completionCommands: CommandFactory;
|
|
18
|
+
export interface CompletionModel {
|
|
19
|
+
/** Top-level command names (excluding commander's built-in `help`). */
|
|
20
|
+
commands: string[];
|
|
21
|
+
/** Subcommand names keyed by their parent command name. */
|
|
22
|
+
subcommands: Record<string, string[]>;
|
|
23
|
+
/** Long flags valid on every command. */
|
|
24
|
+
globalFlags: string[];
|
|
25
|
+
/** Human descriptions for the shells that render them (zsh, fish). */
|
|
26
|
+
descriptions: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the completion model by registering the real command tree onto a
|
|
30
|
+
* throwaway program. The runner is a no-op: completion needs each command's
|
|
31
|
+
* name, options, and subcommands, never its behaviour.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildModel(): Promise<CompletionModel>;
|
|
34
|
+
export declare function renderScript(shell: Shell, model: CompletionModel): string;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { UsageError } from '../core/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shell completion scripts, generated from the live command tree.
|
|
5
|
+
*
|
|
6
|
+
* Rather than hand-maintain a completion file per shell — the exact docs-rot
|
|
7
|
+
* trap a CLI that places real orders cannot afford — this walks the same
|
|
8
|
+
* registration path run() uses (with a no-op runner) and emits command names,
|
|
9
|
+
* subcommand names, and long flags straight from it. A new command or flag is
|
|
10
|
+
* completable as soon as it is added; the only user step is to regenerate after
|
|
11
|
+
* upgrading, the same contract gh, rustup, and kubectl ship.
|
|
12
|
+
*
|
|
13
|
+
* Depth is intentionally capped at command → subcommand + flags, which covers
|
|
14
|
+
* every path this CLI has (`config set`, `margins order`, `orders place`).
|
|
15
|
+
*/
|
|
16
|
+
const SHELLS = ['bash', 'zsh', 'fish'];
|
|
17
|
+
export const completionCommands = (program, run) => {
|
|
18
|
+
program
|
|
19
|
+
.command('completion')
|
|
20
|
+
.summary('Print a shell completion script')
|
|
21
|
+
.description('Print a shell completion script for bash, zsh, or fish.\n\n' +
|
|
22
|
+
'The shell is auto-detected from $SHELL when omitted. Install with, e.g.:\n' +
|
|
23
|
+
' bash kite completion bash >> ~/.bash_completion\n' +
|
|
24
|
+
' zsh kite completion zsh > ~/.zfunc/_kite (with ~/.zfunc on your fpath)\n' +
|
|
25
|
+
' fish kite completion fish > ~/.config/fish/completions/kite.fish')
|
|
26
|
+
.argument('[shell]', 'Target shell: bash, zsh, or fish')
|
|
27
|
+
.action(run(completion));
|
|
28
|
+
};
|
|
29
|
+
async function completion(ctx, _opts, command) {
|
|
30
|
+
const requested = (command.args[0] ?? detectShell())?.toLowerCase();
|
|
31
|
+
if (!requested) {
|
|
32
|
+
throw new UsageError('Could not detect your shell from $SHELL.', `Name it explicitly: kite completion <${SHELLS.join('|')}>.`);
|
|
33
|
+
}
|
|
34
|
+
if (!isShell(requested)) {
|
|
35
|
+
throw new UsageError(`Unsupported shell "${requested}".`, `Supported shells: ${SHELLS.join(', ')}.`);
|
|
36
|
+
}
|
|
37
|
+
const model = await buildModel();
|
|
38
|
+
// The script is data: it goes to stdout so `kite completion bash > file`
|
|
39
|
+
// captures exactly the script and nothing else.
|
|
40
|
+
ctx.io.line(renderScript(requested, model));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the completion model by registering the real command tree onto a
|
|
44
|
+
* throwaway program. The runner is a no-op: completion needs each command's
|
|
45
|
+
* name, options, and subcommands, never its behaviour.
|
|
46
|
+
*/
|
|
47
|
+
export async function buildModel() {
|
|
48
|
+
const { applyGlobalOptions, registerCommands } = await import('./register.js');
|
|
49
|
+
const program = new Command('kite');
|
|
50
|
+
applyGlobalOptions(program);
|
|
51
|
+
const noop = () => async () => { };
|
|
52
|
+
await registerCommands(program, noop);
|
|
53
|
+
const descriptions = {};
|
|
54
|
+
const subcommands = {};
|
|
55
|
+
const commands = [];
|
|
56
|
+
for (const cmd of realCommands(program)) {
|
|
57
|
+
commands.push(cmd.name());
|
|
58
|
+
descriptions[cmd.name()] = summaryOf(cmd);
|
|
59
|
+
const subs = realCommands(cmd);
|
|
60
|
+
if (subs.length > 0) {
|
|
61
|
+
subcommands[cmd.name()] = subs.map((s) => s.name());
|
|
62
|
+
for (const sub of subs)
|
|
63
|
+
descriptions[`${cmd.name()} ${sub.name()}`] = summaryOf(sub);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { commands, subcommands, globalFlags: longFlags(program), descriptions };
|
|
67
|
+
}
|
|
68
|
+
/** Child commands, minus commander's auto-generated `help` command. */
|
|
69
|
+
function realCommands(cmd) {
|
|
70
|
+
return cmd.commands.filter((c) => c.name() !== 'help');
|
|
71
|
+
}
|
|
72
|
+
function longFlags(cmd) {
|
|
73
|
+
return cmd.options.map((o) => o.long).filter((long) => Boolean(long));
|
|
74
|
+
}
|
|
75
|
+
function summaryOf(cmd) {
|
|
76
|
+
// summary() is the short one-liner; description() may be a multi-line block.
|
|
77
|
+
const text = cmd.summary() || cmd.description().split('\n')[0] || '';
|
|
78
|
+
return sanitize(text);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Strip characters that would break a quoted description in a shell script.
|
|
82
|
+
* Backslash is included: fish treats it as an escape inside single quotes, so a
|
|
83
|
+
* description ending in `\` would escape the closing quote and corrupt the line.
|
|
84
|
+
*/
|
|
85
|
+
function sanitize(text) {
|
|
86
|
+
return text
|
|
87
|
+
.replace(/['\\\n\r]/g, ' ')
|
|
88
|
+
.replace(/\s+/g, ' ')
|
|
89
|
+
.trim();
|
|
90
|
+
}
|
|
91
|
+
// --- rendering -------------------------------------------------------------
|
|
92
|
+
export function renderScript(shell, model) {
|
|
93
|
+
// Sanitise at the rendering boundary, not only in buildModel(), so renderScript
|
|
94
|
+
// can never emit a script broken by a stray quote or backslash regardless of
|
|
95
|
+
// how its model was built.
|
|
96
|
+
const safe = {
|
|
97
|
+
...model,
|
|
98
|
+
descriptions: Object.fromEntries(Object.entries(model.descriptions).map(([key, value]) => [key, sanitize(value)])),
|
|
99
|
+
};
|
|
100
|
+
switch (shell) {
|
|
101
|
+
case 'bash':
|
|
102
|
+
return renderBash(safe);
|
|
103
|
+
case 'zsh':
|
|
104
|
+
return renderZsh(safe);
|
|
105
|
+
case 'fish':
|
|
106
|
+
return renderFish(safe);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function renderBash(model) {
|
|
110
|
+
// A `case` for subcommands rather than an associative array, so this works on
|
|
111
|
+
// the bash 3.2 that macOS still ships (declare -A is bash 4+).
|
|
112
|
+
const subCases = Object.entries(model.subcommands)
|
|
113
|
+
.map(([cmd, subs]) => ` ${cmd}) echo "${subs.join(' ')}" ;;`)
|
|
114
|
+
.join('\n');
|
|
115
|
+
return `# kite bash completion. Source it, or install with:
|
|
116
|
+
# kite completion bash >> ~/.bash_completion
|
|
117
|
+
_kite_subcommands() {
|
|
118
|
+
case "$1" in
|
|
119
|
+
${subCases}
|
|
120
|
+
esac
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_kite() {
|
|
124
|
+
local cur cmd sub i w
|
|
125
|
+
COMPREPLY=()
|
|
126
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
127
|
+
|
|
128
|
+
# First two non-flag words after "kite" are the command and its subcommand.
|
|
129
|
+
cmd=""; sub=""
|
|
130
|
+
for (( i=1; i < COMP_CWORD; i++ )); do
|
|
131
|
+
w="\${COMP_WORDS[i]}"
|
|
132
|
+
[[ "$w" == -* ]] && continue
|
|
133
|
+
if [[ -z "$cmd" ]]; then cmd="$w"; else sub="$w"; break; fi
|
|
134
|
+
done
|
|
135
|
+
|
|
136
|
+
if [[ "$cur" == -* ]]; then
|
|
137
|
+
COMPREPLY=( $(compgen -W "${model.globalFlags.join(' ')}" -- "$cur") )
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [[ -z "$cmd" ]]; then
|
|
142
|
+
COMPREPLY=( $(compgen -W "${model.commands.join(' ')}" -- "$cur") )
|
|
143
|
+
return
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
if [[ -z "$sub" ]]; then
|
|
147
|
+
local subs
|
|
148
|
+
subs="$(_kite_subcommands "$cmd")"
|
|
149
|
+
[[ -n "$subs" ]] && COMPREPLY=( $(compgen -W "$subs" -- "$cur") )
|
|
150
|
+
fi
|
|
151
|
+
}
|
|
152
|
+
complete -F _kite kite
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
function renderZsh(model) {
|
|
156
|
+
const topDescribe = model.commands.map((name) => ` '${name}:${model.descriptions[name] ?? ''}'`).join('\n');
|
|
157
|
+
// Build the sub arrays inline per command so descriptions survive.
|
|
158
|
+
const subBlocks = Object.entries(model.subcommands)
|
|
159
|
+
.map(([cmd, subs]) => {
|
|
160
|
+
const described = subs.map((s) => `'${s}:${model.descriptions[`${cmd} ${s}`] ?? ''}'`).join(' ');
|
|
161
|
+
return ` ${cmd}) local -a subs=(${described}); _describe 'subcommand' subs ;;`;
|
|
162
|
+
})
|
|
163
|
+
.join('\n');
|
|
164
|
+
return `#compdef kite
|
|
165
|
+
# kite zsh completion. Install with:
|
|
166
|
+
# kite completion zsh > "\${fpath[1]}/_kite"
|
|
167
|
+
_kite() {
|
|
168
|
+
local -a commands
|
|
169
|
+
commands=(
|
|
170
|
+
${topDescribe}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Flags complete anywhere the current word starts with a dash. \`compadd --\`
|
|
174
|
+
# is required: a bare \`compadd --json\` makes zsh parse the flags as its own
|
|
175
|
+
# options instead of as candidates.
|
|
176
|
+
if [[ \${words[CURRENT]} == -* ]]; then
|
|
177
|
+
compadd -- ${model.globalFlags.join(' ')}
|
|
178
|
+
return
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Top-level command name.
|
|
182
|
+
if (( CURRENT == 2 )); then
|
|
183
|
+
_describe 'command' commands
|
|
184
|
+
return
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# Subcommands only at the subcommand position; deeper positions offer nothing,
|
|
188
|
+
# which caps depth at command -> subcommand as documented.
|
|
189
|
+
if (( CURRENT == 3 )); then
|
|
190
|
+
case \${words[2]} in
|
|
191
|
+
${subBlocks}
|
|
192
|
+
esac
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
compdef _kite kite
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function renderFish(model) {
|
|
199
|
+
const lines = [
|
|
200
|
+
'# kite fish completion. Install with:',
|
|
201
|
+
'# kite completion fish > ~/.config/fish/completions/kite.fish',
|
|
202
|
+
'',
|
|
203
|
+
'# No file completion by default; kite takes symbols and subcommands, not paths.',
|
|
204
|
+
'complete -c kite -f',
|
|
205
|
+
'',
|
|
206
|
+
'# Top-level commands (only before a subcommand is typed).',
|
|
207
|
+
];
|
|
208
|
+
for (const name of model.commands) {
|
|
209
|
+
lines.push(`complete -c kite -n __fish_use_subcommand -a ${name} -d '${model.descriptions[name] ?? ''}'`);
|
|
210
|
+
}
|
|
211
|
+
lines.push('', '# Subcommands (only until one is chosen, capping depth at command → subcommand).');
|
|
212
|
+
for (const [cmd, subs] of Object.entries(model.subcommands)) {
|
|
213
|
+
// __fish_seen_subcommand_from is true anywhere the token appears on the
|
|
214
|
+
// line, so without the `and not …` guard `kite config set <TAB>` would keep
|
|
215
|
+
// re-offering config's subcommands. Excluding the subcommands themselves
|
|
216
|
+
// stops that once one has been typed.
|
|
217
|
+
const guard = `__fish_seen_subcommand_from ${cmd}; and not __fish_seen_subcommand_from ${subs.join(' ')}`;
|
|
218
|
+
for (const sub of subs) {
|
|
219
|
+
const desc = model.descriptions[`${cmd} ${sub}`] ?? '';
|
|
220
|
+
lines.push(`complete -c kite -n '${guard}' -a ${sub} -d '${desc}'`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
lines.push('', '# Global flags.');
|
|
224
|
+
for (const flag of model.globalFlags) {
|
|
225
|
+
lines.push(`complete -c kite -l ${flag.replace(/^--/, '')}`);
|
|
226
|
+
}
|
|
227
|
+
return `${lines.join('\n')}\n`;
|
|
228
|
+
}
|
|
229
|
+
// --- helpers ---------------------------------------------------------------
|
|
230
|
+
function isShell(value) {
|
|
231
|
+
return SHELLS.includes(value);
|
|
232
|
+
}
|
|
233
|
+
/** Best-effort shell detection from $SHELL, e.g. "/usr/bin/fish" -> "fish". */
|
|
234
|
+
function detectShell() {
|
|
235
|
+
const shell = process.env['SHELL'];
|
|
236
|
+
if (!shell)
|
|
237
|
+
return undefined;
|
|
238
|
+
const base = shell.split('/').pop();
|
|
239
|
+
return base && isShell(base) ? base : undefined;
|
|
240
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { getSecret, keyringAvailable } from '../core/credentials.js';
|
|
4
|
+
import { ExitCode } from '../core/errors.js';
|
|
5
|
+
import { configFile } from '../core/paths.js';
|
|
6
|
+
import { isExpired, timeUntilExpiry } from '../core/session.js';
|
|
7
|
+
import { renderKeyValue } from '../output/table.js';
|
|
8
|
+
export const doctorCommands = (program, run) => {
|
|
9
|
+
program
|
|
10
|
+
.command('doctor')
|
|
11
|
+
.description('Run offline health checks on your configuration, credentials, and session')
|
|
12
|
+
.action(run(doctor));
|
|
13
|
+
};
|
|
14
|
+
async function doctor(ctx) {
|
|
15
|
+
const checks = [
|
|
16
|
+
checkNode(),
|
|
17
|
+
await checkConfigFile(),
|
|
18
|
+
await checkKeyring(),
|
|
19
|
+
await checkCredentials(ctx),
|
|
20
|
+
checkSession(ctx),
|
|
21
|
+
await checkCallbackPort(ctx),
|
|
22
|
+
];
|
|
23
|
+
const worst = checks.some((c) => c.status === 'fail')
|
|
24
|
+
? 'fail'
|
|
25
|
+
: checks.some((c) => c.status === 'warn')
|
|
26
|
+
? 'warn'
|
|
27
|
+
: 'ok';
|
|
28
|
+
if (ctx.io.json) {
|
|
29
|
+
ctx.io.writeJson({
|
|
30
|
+
ok: worst !== 'fail',
|
|
31
|
+
profile: ctx.profile.name,
|
|
32
|
+
env: ctx.env,
|
|
33
|
+
checks: checks.map((c) => ({
|
|
34
|
+
name: c.name,
|
|
35
|
+
status: c.status,
|
|
36
|
+
detail: c.detail,
|
|
37
|
+
...(c.hint ? { hint: c.hint } : {}),
|
|
38
|
+
})),
|
|
39
|
+
});
|
|
40
|
+
if (worst === 'fail')
|
|
41
|
+
process.exitCode = ExitCode.Failure;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { io } = ctx;
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const check of checks) {
|
|
47
|
+
rows.push([`${icon(io, check.status)} ${check.name}`, check.detail]);
|
|
48
|
+
if (check.hint)
|
|
49
|
+
rows.push(['', io.dim(check.hint)]);
|
|
50
|
+
}
|
|
51
|
+
io.line(renderKeyValue(io, rows));
|
|
52
|
+
io.note('');
|
|
53
|
+
if (worst === 'fail') {
|
|
54
|
+
io.error('Some checks failed. See the hints above.');
|
|
55
|
+
process.exitCode = ExitCode.Failure;
|
|
56
|
+
}
|
|
57
|
+
else if (worst === 'warn') {
|
|
58
|
+
io.warn('All essential checks passed, with warnings.');
|
|
59
|
+
io.info('Run `kite whoami` to confirm the session is live on Kite (doctor is offline).');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
io.success('Everything looks healthy.');
|
|
63
|
+
io.info('Run `kite whoami` to confirm the session against Kite (doctor is offline).');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// --- individual checks -----------------------------------------------------
|
|
67
|
+
/** Node must meet the floor declared in package.json `engines`. */
|
|
68
|
+
function checkNode() {
|
|
69
|
+
const current = process.versions.node;
|
|
70
|
+
const floor = '22.12.0';
|
|
71
|
+
return meetsFloor(current, floor)
|
|
72
|
+
? { name: 'Node runtime', status: 'ok', detail: `v${current}` }
|
|
73
|
+
: {
|
|
74
|
+
name: 'Node runtime',
|
|
75
|
+
status: 'fail',
|
|
76
|
+
detail: `v${current} is below the required v${floor}`,
|
|
77
|
+
hint: `Upgrade to Node ${floor} or newer.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function checkConfigFile() {
|
|
81
|
+
const path = configFile();
|
|
82
|
+
let info;
|
|
83
|
+
try {
|
|
84
|
+
info = await stat(path);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.code === 'ENOENT') {
|
|
88
|
+
// No file is fine — defaults apply until the first `config set`.
|
|
89
|
+
return { name: 'Config file', status: 'ok', detail: 'none yet (using built-in defaults)' };
|
|
90
|
+
}
|
|
91
|
+
return { name: 'Config file', status: 'fail', detail: `cannot read ${path}`, hint: err.message };
|
|
92
|
+
}
|
|
93
|
+
// Windows models permissions through ACLs, not the POSIX mode bits, so the
|
|
94
|
+
// 0600 assertion only makes sense off Windows.
|
|
95
|
+
if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) {
|
|
96
|
+
const mode = (info.mode & 0o777).toString(8).padStart(3, '0');
|
|
97
|
+
return {
|
|
98
|
+
name: 'Config file',
|
|
99
|
+
status: 'warn',
|
|
100
|
+
detail: `${path} is mode ${mode} (world- or group-readable)`,
|
|
101
|
+
hint: `Tighten it: chmod 600 ${path}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { name: 'Config file', status: 'ok', detail: path };
|
|
105
|
+
}
|
|
106
|
+
async function checkKeyring() {
|
|
107
|
+
return (await keyringAvailable())
|
|
108
|
+
? { name: 'Credential store', status: 'ok', detail: 'OS keyring available' }
|
|
109
|
+
: {
|
|
110
|
+
name: 'Credential store',
|
|
111
|
+
status: 'warn',
|
|
112
|
+
detail: 'no OS keyring — falling back to an encrypted file',
|
|
113
|
+
hint: 'Set KITE_CREDENTIALS_PASSPHRASE to enable the encrypted file store on this machine.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function checkCredentials(ctx) {
|
|
117
|
+
if (ctx.env === 'sandbox') {
|
|
118
|
+
return { name: 'API secret', status: 'ok', detail: 'using the public sandbox credentials' };
|
|
119
|
+
}
|
|
120
|
+
const secret = await getSecret('api_secret', { scope: ctx.credentialScope });
|
|
121
|
+
return secret
|
|
122
|
+
? { name: 'API secret', status: 'ok', detail: `stored (${secret.backend})` }
|
|
123
|
+
: {
|
|
124
|
+
name: 'API secret',
|
|
125
|
+
status: 'warn',
|
|
126
|
+
detail: 'not stored',
|
|
127
|
+
hint: 'Run `kite login` (or set KITE_API_SECRET) so re-login and signing work.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Cached session metadata only — liveness against Kite is `kite whoami`'s job. */
|
|
131
|
+
function checkSession(ctx) {
|
|
132
|
+
const loginHint = ctx.profile.name === 'default' ? 'Run `kite login`.' : `Run \`kite --profile ${ctx.profile.name} login\`.`;
|
|
133
|
+
if (!ctx.session) {
|
|
134
|
+
return { name: 'Session', status: 'warn', detail: 'not logged in', hint: loginHint };
|
|
135
|
+
}
|
|
136
|
+
if (isExpired(ctx.session)) {
|
|
137
|
+
return {
|
|
138
|
+
name: 'Session',
|
|
139
|
+
status: 'warn',
|
|
140
|
+
detail: 'expired (Kite invalidates tokens at 06:00 IST daily)',
|
|
141
|
+
hint: loginHint,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const who = ctx.session.userName ?? ctx.session.userId;
|
|
145
|
+
return { name: 'Session', status: 'ok', detail: `${who}, expires in ${timeUntilExpiry(ctx.session)}` };
|
|
146
|
+
}
|
|
147
|
+
async function checkCallbackPort(ctx) {
|
|
148
|
+
const port = ctx.config.redirectPort;
|
|
149
|
+
const free = await portIsFree(port);
|
|
150
|
+
return free
|
|
151
|
+
? { name: 'Login callback port', status: 'ok', detail: `127.0.0.1:${port} is free` }
|
|
152
|
+
: {
|
|
153
|
+
name: 'Login callback port',
|
|
154
|
+
status: 'warn',
|
|
155
|
+
detail: `127.0.0.1:${port} is in use`,
|
|
156
|
+
hint: 'Another process holds the callback port; `kite login` may fail until it frees, or change redirectPort.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// --- helpers ---------------------------------------------------------------
|
|
160
|
+
function icon(io, status) {
|
|
161
|
+
switch (status) {
|
|
162
|
+
case 'ok':
|
|
163
|
+
return io.green('✓');
|
|
164
|
+
case 'warn':
|
|
165
|
+
return io.yellow('!');
|
|
166
|
+
case 'fail':
|
|
167
|
+
return io.red('✗');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** True when `current` is >= `floor`, comparing major.minor.patch numerically. */
|
|
171
|
+
export function meetsFloor(current, floor) {
|
|
172
|
+
const c = current.split('.').map((n) => Number.parseInt(n, 10));
|
|
173
|
+
const f = floor.split('.').map((n) => Number.parseInt(n, 10));
|
|
174
|
+
for (let i = 0; i < 3; i++) {
|
|
175
|
+
const a = c[i] ?? 0;
|
|
176
|
+
const b = f[i] ?? 0;
|
|
177
|
+
if (a !== b)
|
|
178
|
+
return a > b;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** Bind-and-release probe: can `kite login` open its loopback callback server? */
|
|
183
|
+
function portIsFree(port, host = '127.0.0.1') {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const server = createServer();
|
|
186
|
+
server.once('error', () => resolve(false));
|
|
187
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
188
|
+
server.listen(port, host);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Runner } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command wiring, factored out of run.ts so there is a single source of truth
|
|
5
|
+
* for the command tree.
|
|
6
|
+
*
|
|
7
|
+
* run() consumes this to build the live program; `kite completion` consumes it
|
|
8
|
+
* with a no-op runner to introspect command and flag names for shell
|
|
9
|
+
* completions. Keeping both on the same registration path means a new command
|
|
10
|
+
* or flag is completable the moment it is added, with nothing to keep in sync.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Attach the global options every command inherits. Split from the name /
|
|
14
|
+
* version / output wiring in run.ts, which is invocation-specific, so an
|
|
15
|
+
* introspection-only program can still enumerate the same global flags.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyGlobalOptions(program: Command): Command;
|
|
18
|
+
/**
|
|
19
|
+
* Register every command group on `program`, grouped for a readable `--help`.
|
|
20
|
+
*
|
|
21
|
+
* The `run` runner wraps each handler with context construction and error
|
|
22
|
+
* reporting in the real CLI; completion passes a no-op that only needs the
|
|
23
|
+
* command definitions, not their behaviour.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerCommands(program: Command, run: Runner): Promise<void>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command wiring, factored out of run.ts so there is a single source of truth
|
|
3
|
+
* for the command tree.
|
|
4
|
+
*
|
|
5
|
+
* run() consumes this to build the live program; `kite completion` consumes it
|
|
6
|
+
* with a no-op runner to introspect command and flag names for shell
|
|
7
|
+
* completions. Keeping both on the same registration path means a new command
|
|
8
|
+
* or flag is completable the moment it is added, with nothing to keep in sync.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Attach the global options every command inherits. Split from the name /
|
|
12
|
+
* version / output wiring in run.ts, which is invocation-specific, so an
|
|
13
|
+
* introspection-only program can still enumerate the same global flags.
|
|
14
|
+
*/
|
|
15
|
+
export function applyGlobalOptions(program) {
|
|
16
|
+
return (program
|
|
17
|
+
.option('--json', 'Emit JSON instead of formatted tables')
|
|
18
|
+
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
19
|
+
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
20
|
+
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
21
|
+
.option('--quiet', 'Suppress informational messages')
|
|
22
|
+
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
23
|
+
.option('--env <env>', 'Environment: production or sandbox')
|
|
24
|
+
.option('--profile <name>', 'Account profile to use (see `kite profiles`)')
|
|
25
|
+
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
26
|
+
.option('--dry-run', 'Show what would happen without sending anything to Kite'));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register every command group on `program`, grouped for a readable `--help`.
|
|
30
|
+
*
|
|
31
|
+
* The `run` runner wraps each handler with context construction and error
|
|
32
|
+
* reporting in the real CLI; completion passes a no-op that only needs the
|
|
33
|
+
* command definitions, not their behaviour.
|
|
34
|
+
*/
|
|
35
|
+
export async function registerCommands(program, run) {
|
|
36
|
+
// Imported here rather than at module top so a single invocation only pays for
|
|
37
|
+
// parsing the command modules once run() (or completion) actually asks for
|
|
38
|
+
// them, not on every `import` of this file.
|
|
39
|
+
const { authCommands } = await import('./auth.js');
|
|
40
|
+
const { profileCommands } = await import('./profiles.js');
|
|
41
|
+
const { portfolioCommands } = await import('./portfolio.js');
|
|
42
|
+
const { marketCommands } = await import('./market.js');
|
|
43
|
+
const { orderCommands } = await import('./orders.js');
|
|
44
|
+
const { gttCommands } = await import('./gtt.js');
|
|
45
|
+
const { alertCommands } = await import('./alerts.js');
|
|
46
|
+
const { marginCommands } = await import('./margins.js');
|
|
47
|
+
const { mfCommands } = await import('./mf.js');
|
|
48
|
+
const { watchCommands } = await import('./watch.js');
|
|
49
|
+
const { configCommands } = await import('./config.js');
|
|
50
|
+
const { doctorCommands } = await import('./doctor.js');
|
|
51
|
+
const { completionCommands } = await import('./completion.js');
|
|
52
|
+
// commandsGroup applies to every command registered after it, so the group
|
|
53
|
+
// is set immediately before each block. With ~25 commands this is the
|
|
54
|
+
// difference between a readable --help and a wall of text.
|
|
55
|
+
program.commandsGroup('Account:');
|
|
56
|
+
authCommands(program, run);
|
|
57
|
+
profileCommands(program, run);
|
|
58
|
+
program.commandsGroup('Portfolio:');
|
|
59
|
+
portfolioCommands(program, run);
|
|
60
|
+
program.commandsGroup('Mutual funds:');
|
|
61
|
+
mfCommands(program, run);
|
|
62
|
+
program.commandsGroup('Market data:');
|
|
63
|
+
marketCommands(program, run);
|
|
64
|
+
program.commandsGroup('Trading:');
|
|
65
|
+
orderCommands(program, run);
|
|
66
|
+
gttCommands(program, run);
|
|
67
|
+
alertCommands(program, run);
|
|
68
|
+
marginCommands(program, run);
|
|
69
|
+
program.commandsGroup('Streaming:');
|
|
70
|
+
watchCommands(program, run);
|
|
71
|
+
program.commandsGroup('Settings:');
|
|
72
|
+
configCommands(program, run);
|
|
73
|
+
doctorCommands(program, run);
|
|
74
|
+
completionCommands(program, run);
|
|
75
|
+
}
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -59,6 +59,15 @@ export declare function waitForCallback(opts: CallbackServerOptions): {
|
|
|
59
59
|
close: () => void;
|
|
60
60
|
};
|
|
61
61
|
export declare function redirectUrlFor(port: number, path: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Copy text to the OS clipboard, returning whether it succeeded.
|
|
64
|
+
*
|
|
65
|
+
* Like {@link openBrowser}, the text is piped to a fixed binary's stdin — never
|
|
66
|
+
* through a shell and never as an argv element — so a URL's `&`/`=` cannot
|
|
67
|
+
* become word-splitting or command injection. On Linux there is no single
|
|
68
|
+
* clipboard tool, so we try the common ones in turn (Wayland first, then X11).
|
|
69
|
+
*/
|
|
70
|
+
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
62
71
|
/**
|
|
63
72
|
* Open a URL in the user's default browser.
|
|
64
73
|
*
|
package/dist/core/auth.js
CHANGED
|
@@ -175,6 +175,45 @@ function escapeHtml(value) {
|
|
|
175
175
|
export function redirectUrlFor(port, path) {
|
|
176
176
|
return `http://127.0.0.1:${port}${path}`;
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Copy text to the OS clipboard, returning whether it succeeded.
|
|
180
|
+
*
|
|
181
|
+
* Like {@link openBrowser}, the text is piped to a fixed binary's stdin — never
|
|
182
|
+
* through a shell and never as an argv element — so a URL's `&`/`=` cannot
|
|
183
|
+
* become word-splitting or command injection. On Linux there is no single
|
|
184
|
+
* clipboard tool, so we try the common ones in turn (Wayland first, then X11).
|
|
185
|
+
*/
|
|
186
|
+
export async function copyToClipboard(text) {
|
|
187
|
+
const candidates = process.platform === 'darwin'
|
|
188
|
+
? [['pbcopy', []]]
|
|
189
|
+
: process.platform === 'win32'
|
|
190
|
+
? [['clip', []]]
|
|
191
|
+
: [
|
|
192
|
+
['wl-copy', []],
|
|
193
|
+
['xclip', ['-selection', 'clipboard']],
|
|
194
|
+
['xsel', ['--clipboard', '--input']],
|
|
195
|
+
];
|
|
196
|
+
for (const [command, args] of candidates) {
|
|
197
|
+
if (await pipeToCommand(command, args, text))
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
/** Spawn `command`, write `text` to its stdin, resolve true on a clean exit. */
|
|
203
|
+
function pipeToCommand(command, args, text) {
|
|
204
|
+
return import('node:child_process').then(({ spawn }) => new Promise((resolve) => {
|
|
205
|
+
try {
|
|
206
|
+
const child = spawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'], shell: false });
|
|
207
|
+
child.on('error', () => resolve(false));
|
|
208
|
+
child.on('close', (code) => resolve(code === 0));
|
|
209
|
+
child.stdin.on('error', () => resolve(false));
|
|
210
|
+
child.stdin.end(text);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
resolve(false);
|
|
214
|
+
}
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
178
217
|
/**
|
|
179
218
|
* Open a URL in the user's default browser.
|
|
180
219
|
*
|
package/dist/core/client.js
CHANGED
|
@@ -177,7 +177,14 @@ export class KiteClient {
|
|
|
177
177
|
return new KiteCliError('Cancelled.', ExitCode.Aborted);
|
|
178
178
|
}
|
|
179
179
|
const message = err instanceof Error ? err.message : String(err);
|
|
180
|
-
|
|
180
|
+
// undici's fetch wraps the actual failure (ECONNRESET, ENOTFOUND, a TLS
|
|
181
|
+
// error, ...) in `cause` and sets `message` to the unhelpful constant
|
|
182
|
+
// "fetch failed" — surface the cause too, or every transient network
|
|
183
|
+
// problem looks identical and undebuggable.
|
|
184
|
+
const cause = err instanceof Error ? err.cause : undefined;
|
|
185
|
+
const causeMessage = cause instanceof Error ? cause.message : cause !== undefined ? String(cause) : undefined;
|
|
186
|
+
const detail = causeMessage && causeMessage !== message ? `${message}: ${causeMessage}` : message;
|
|
187
|
+
return new NetworkError(`Could not reach Kite: ${redactString(detail)}`, 'Check your network connection.');
|
|
181
188
|
}
|
|
182
189
|
handleResponse(response, text, schema, url) {
|
|
183
190
|
let payload;
|
package/dist/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { Command, CommanderError } from 'commander';
|
|
3
|
+
import { applyGlobalOptions, registerCommands } from './commands/register.js';
|
|
3
4
|
import { createContext } from './context.js';
|
|
4
5
|
import { AbortedError, ExitCode, KiteCliError } from './core/errors.js';
|
|
5
6
|
import { redactString } from './core/redact.js';
|
|
@@ -36,16 +37,6 @@ export async function run(opts = {}) {
|
|
|
36
37
|
.name('kite')
|
|
37
38
|
.description('Unofficial command-line interface for the Zerodha Kite Connect API (not affiliated with Zerodha)')
|
|
38
39
|
.version(VERSION, '-v, --version')
|
|
39
|
-
.option('--json', 'Emit JSON instead of formatted tables')
|
|
40
|
-
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
41
|
-
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
42
|
-
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
43
|
-
.option('--quiet', 'Suppress informational messages')
|
|
44
|
-
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
45
|
-
.option('--env <env>', 'Environment: production or sandbox')
|
|
46
|
-
.option('--profile <name>', 'Account profile to use (see `kite profiles`)')
|
|
47
|
-
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
48
|
-
.option('--dry-run', 'Show what would happen without sending anything to Kite')
|
|
49
40
|
.showHelpAfterError('(run `kite --help` for usage)')
|
|
50
41
|
.configureOutput({
|
|
51
42
|
writeOut: (str) => (opts.streams?.stdout ?? process.stdout).write(str),
|
|
@@ -54,6 +45,7 @@ export async function run(opts = {}) {
|
|
|
54
45
|
// Throw instead of calling process.exit, so `run()` stays testable and
|
|
55
46
|
// always returns its exit code.
|
|
56
47
|
.exitOverride();
|
|
48
|
+
applyGlobalOptions(program);
|
|
57
49
|
/** Wraps a handler with context construction and error reporting. */
|
|
58
50
|
const withContext = (handler) => async (...args) => {
|
|
59
51
|
// Commander passes (…arguments, options, command).
|
|
@@ -68,40 +60,11 @@ export async function run(opts = {}) {
|
|
|
68
60
|
exitCode = reportError(err, ctx.io);
|
|
69
61
|
}
|
|
70
62
|
};
|
|
71
|
-
// Registered
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { marketCommands } = await import('./commands/market.js');
|
|
77
|
-
const { orderCommands } = await import('./commands/orders.js');
|
|
78
|
-
const { gttCommands } = await import('./commands/gtt.js');
|
|
79
|
-
const { alertCommands } = await import('./commands/alerts.js');
|
|
80
|
-
const { marginCommands } = await import('./commands/margins.js');
|
|
81
|
-
const { mfCommands } = await import('./commands/mf.js');
|
|
82
|
-
const { watchCommands } = await import('./commands/watch.js');
|
|
83
|
-
const { configCommands } = await import('./commands/config.js');
|
|
84
|
-
// commandsGroup applies to every command registered after it, so the group
|
|
85
|
-
// is set immediately before each block. With ~25 commands this is the
|
|
86
|
-
// difference between a readable --help and a wall of text.
|
|
87
|
-
program.commandsGroup('Account:');
|
|
88
|
-
authCommands(program, withContext);
|
|
89
|
-
profileCommands(program, withContext);
|
|
90
|
-
program.commandsGroup('Portfolio:');
|
|
91
|
-
portfolioCommands(program, withContext);
|
|
92
|
-
program.commandsGroup('Mutual funds:');
|
|
93
|
-
mfCommands(program, withContext);
|
|
94
|
-
program.commandsGroup('Market data:');
|
|
95
|
-
marketCommands(program, withContext);
|
|
96
|
-
program.commandsGroup('Trading:');
|
|
97
|
-
orderCommands(program, withContext);
|
|
98
|
-
gttCommands(program, withContext);
|
|
99
|
-
alertCommands(program, withContext);
|
|
100
|
-
marginCommands(program, withContext);
|
|
101
|
-
program.commandsGroup('Streaming:');
|
|
102
|
-
watchCommands(program, withContext);
|
|
103
|
-
program.commandsGroup('Settings:');
|
|
104
|
-
configCommands(program, withContext);
|
|
63
|
+
// Registered through the shared registration path (see commands/register.ts)
|
|
64
|
+
// so run() and `kite completion` build the exact same command tree. Modules
|
|
65
|
+
// are still imported lazily inside registerCommands, so a bare `kite quote`
|
|
66
|
+
// never pays to parse the dashboard renderer or the ticker up front.
|
|
67
|
+
await registerCommands(program, withContext);
|
|
105
68
|
program.addHelpText('after', `
|
|
106
69
|
Examples:
|
|
107
70
|
$ kite login Authenticate and store a session
|