@pungoyal/kite-cli 0.2.1 → 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 +50 -1
- package/dist/commands/alerts.d.ts +28 -0
- package/dist/commands/alerts.js +257 -76
- 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/margins.d.ts +34 -0
- package/dist/commands/margins.js +258 -0
- package/dist/commands/mf.d.ts +10 -0
- package/dist/commands/mf.js +85 -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/core/schemas.d.ts +3 -0
- package/dist/run.js +7 -39
- package/package.json +2 -2
|
@@ -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,34 @@
|
|
|
1
|
+
import { type OrderType, type Product, type TransactionType, type Variety } from '../core/api.js';
|
|
2
|
+
import type { CommandFactory } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Margin and charge calculators — read-only.
|
|
5
|
+
*
|
|
6
|
+
* These POST a hypothetical set of orders to Kite and return what they would
|
|
7
|
+
* cost: `order` gives the per-order required margin (no netting), `basket` the
|
|
8
|
+
* net margin for the set (with spread/hedge benefit), and `charges` the
|
|
9
|
+
* itemised brokerage/tax breakdown (a "virtual contract note"). Nothing is
|
|
10
|
+
* placed, so none of the trading safety layer applies.
|
|
11
|
+
*/
|
|
12
|
+
export declare const marginCommands: CommandFactory;
|
|
13
|
+
/** A hypothetical order, resolved from a spec, ready to serialise per endpoint. */
|
|
14
|
+
export interface OrderSpec {
|
|
15
|
+
exchange: string;
|
|
16
|
+
tradingsymbol: string;
|
|
17
|
+
transactionType: TransactionType;
|
|
18
|
+
quantity: number;
|
|
19
|
+
orderType: OrderType;
|
|
20
|
+
product: Product;
|
|
21
|
+
variety: Variety;
|
|
22
|
+
price: number | undefined;
|
|
23
|
+
triggerPrice: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse one `EXCHANGE:SYMBOL:SIDE:QTY[:attrs...]` spec.
|
|
27
|
+
*
|
|
28
|
+
* Attributes after QTY are classified by content — an order type, a product, a
|
|
29
|
+
* variety, a bare number (the price), or `trigger=<n>`. Fails closed: an
|
|
30
|
+
* unrecognised token, a duplicated field, or an empty field rejects the whole
|
|
31
|
+
* spec rather than silently defaulting, since a mis-parsed order yields a
|
|
32
|
+
* silently wrong margin or charge.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseOrderSpec(spec: string): OrderSpec;
|