@pungoyal/kite-cli 0.1.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/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +265 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +182 -0
- package/dist/commands/gtt.d.ts +12 -0
- package/dist/commands/gtt.js +276 -0
- package/dist/commands/market.d.ts +2 -0
- package/dist/commands/market.js +389 -0
- package/dist/commands/orders.d.ts +2 -0
- package/dist/commands/orders.js +679 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +286 -0
- package/dist/commands/types.d.ts +16 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/watch.d.ts +18 -0
- package/dist/commands/watch.js +285 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +126 -0
- package/dist/core/api.d.ts +843 -0
- package/dist/core/api.js +472 -0
- package/dist/core/auth.d.ts +68 -0
- package/dist/core/auth.js +220 -0
- package/dist/core/client.d.ts +76 -0
- package/dist/core/client.js +285 -0
- package/dist/core/config.d.ts +114 -0
- package/dist/core/config.js +163 -0
- package/dist/core/credentials.d.ts +27 -0
- package/dist/core/credentials.js +182 -0
- package/dist/core/errors.d.ts +83 -0
- package/dist/core/errors.js +152 -0
- package/dist/core/instruments.d.ts +87 -0
- package/dist/core/instruments.js +288 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/ratelimit.d.ts +57 -0
- package/dist/core/ratelimit.js +196 -0
- package/dist/core/redact.d.ts +48 -0
- package/dist/core/redact.js +181 -0
- package/dist/core/schemas.d.ts +662 -0
- package/dist/core/schemas.js +433 -0
- package/dist/core/secretstore.d.ts +11 -0
- package/dist/core/secretstore.js +138 -0
- package/dist/core/session.d.ts +37 -0
- package/dist/core/session.js +102 -0
- package/dist/core/ticker.d.ts +131 -0
- package/dist/core/ticker.js +367 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/output/format.d.ts +31 -0
- package/dist/output/format.js +162 -0
- package/dist/output/io.d.ts +70 -0
- package/dist/output/io.js +143 -0
- package/dist/output/table.d.ts +28 -0
- package/dist/output/table.js +73 -0
- package/dist/run.d.ts +15 -0
- package/dist/run.js +161 -0
- package/dist/safety.d.ts +93 -0
- package/dist/safety.js +154 -0
- package/package.json +85 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number, currency and date formatting.
|
|
3
|
+
*
|
|
4
|
+
* Everything is rendered in the Indian locale and IST, because that is the only
|
|
5
|
+
* frame of reference Kite operates in. Rupee amounts use the Indian digit
|
|
6
|
+
* grouping (lakh/crore), which `en-IN` gives us for free.
|
|
7
|
+
*/
|
|
8
|
+
const INR = new Intl.NumberFormat('en-IN', {
|
|
9
|
+
minimumFractionDigits: 2,
|
|
10
|
+
maximumFractionDigits: 2,
|
|
11
|
+
});
|
|
12
|
+
const INR_COMPACT = new Intl.NumberFormat('en-IN', {
|
|
13
|
+
maximumFractionDigits: 0,
|
|
14
|
+
});
|
|
15
|
+
const QTY = new Intl.NumberFormat('en-IN', { maximumFractionDigits: 0 });
|
|
16
|
+
/** A rupee amount, e.g. "12,34,567.89". No symbol — callers add it. */
|
|
17
|
+
export function money(value) {
|
|
18
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
19
|
+
return '—';
|
|
20
|
+
return INR.format(value);
|
|
21
|
+
}
|
|
22
|
+
/** A rupee amount with the symbol, e.g. "₹12,34,567.89". */
|
|
23
|
+
export function rupees(value) {
|
|
24
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
25
|
+
return '—';
|
|
26
|
+
return `₹${INR.format(value)}`;
|
|
27
|
+
}
|
|
28
|
+
/** A signed rupee amount, e.g. "+₹1,200.00" / "-₹340.50". */
|
|
29
|
+
export function signedRupees(value) {
|
|
30
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
31
|
+
return '—';
|
|
32
|
+
const sign = value > 0 ? '+' : value < 0 ? '-' : '';
|
|
33
|
+
return `${sign}₹${INR.format(Math.abs(value))}`;
|
|
34
|
+
}
|
|
35
|
+
/** Large rupee amounts abbreviated as L / Cr, the conventional Indian units. */
|
|
36
|
+
export function compactRupees(value) {
|
|
37
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
38
|
+
return '—';
|
|
39
|
+
const abs = Math.abs(value);
|
|
40
|
+
const sign = value < 0 ? '-' : '';
|
|
41
|
+
if (abs >= 10_000_000)
|
|
42
|
+
return `${sign}₹${(abs / 10_000_000).toFixed(2)}Cr`;
|
|
43
|
+
if (abs >= 100_000)
|
|
44
|
+
return `${sign}₹${(abs / 100_000).toFixed(2)}L`;
|
|
45
|
+
return `${sign}₹${INR_COMPACT.format(abs)}`;
|
|
46
|
+
}
|
|
47
|
+
export function percent(value, digits = 2) {
|
|
48
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
49
|
+
return '—';
|
|
50
|
+
const sign = value > 0 ? '+' : '';
|
|
51
|
+
return `${sign}${value.toFixed(digits)}%`;
|
|
52
|
+
}
|
|
53
|
+
export function quantity(value) {
|
|
54
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
55
|
+
return '—';
|
|
56
|
+
return QTY.format(value);
|
|
57
|
+
}
|
|
58
|
+
/** Compact volume, e.g. "1.2M". */
|
|
59
|
+
export function compactNumber(value) {
|
|
60
|
+
if (value === undefined || value === null || !Number.isFinite(value))
|
|
61
|
+
return '—';
|
|
62
|
+
const abs = Math.abs(value);
|
|
63
|
+
const sign = value < 0 ? '-' : '';
|
|
64
|
+
if (abs >= 1_000_000_000)
|
|
65
|
+
return `${sign}${(abs / 1_000_000_000).toFixed(1)}B`;
|
|
66
|
+
if (abs >= 1_000_000)
|
|
67
|
+
return `${sign}${(abs / 1_000_000).toFixed(1)}M`;
|
|
68
|
+
if (abs >= 1_000)
|
|
69
|
+
return `${sign}${(abs / 1_000).toFixed(1)}K`;
|
|
70
|
+
return `${sign}${abs}`;
|
|
71
|
+
}
|
|
72
|
+
const IST_DATETIME = new Intl.DateTimeFormat('en-IN', {
|
|
73
|
+
timeZone: 'Asia/Kolkata',
|
|
74
|
+
year: 'numeric',
|
|
75
|
+
month: 'short',
|
|
76
|
+
day: '2-digit',
|
|
77
|
+
hour: '2-digit',
|
|
78
|
+
minute: '2-digit',
|
|
79
|
+
second: '2-digit',
|
|
80
|
+
hour12: false,
|
|
81
|
+
});
|
|
82
|
+
const IST_TIME = new Intl.DateTimeFormat('en-IN', {
|
|
83
|
+
timeZone: 'Asia/Kolkata',
|
|
84
|
+
hour: '2-digit',
|
|
85
|
+
minute: '2-digit',
|
|
86
|
+
second: '2-digit',
|
|
87
|
+
hour12: false,
|
|
88
|
+
});
|
|
89
|
+
const IST_DATE = new Intl.DateTimeFormat('en-CA', {
|
|
90
|
+
timeZone: 'Asia/Kolkata',
|
|
91
|
+
year: 'numeric',
|
|
92
|
+
month: '2-digit',
|
|
93
|
+
day: '2-digit',
|
|
94
|
+
});
|
|
95
|
+
export function dateTime(value) {
|
|
96
|
+
const date = toDate(value);
|
|
97
|
+
return date ? IST_DATETIME.format(date) : '—';
|
|
98
|
+
}
|
|
99
|
+
export function timeOnly(value) {
|
|
100
|
+
const date = toDate(value);
|
|
101
|
+
return date ? IST_TIME.format(date) : '—';
|
|
102
|
+
}
|
|
103
|
+
export function dateOnly(value) {
|
|
104
|
+
const date = toDate(value);
|
|
105
|
+
return date ? IST_DATE.format(date) : '—';
|
|
106
|
+
}
|
|
107
|
+
function toDate(value) {
|
|
108
|
+
if (!value)
|
|
109
|
+
return null;
|
|
110
|
+
if (value instanceof Date)
|
|
111
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
112
|
+
// Kite returns "YYYY-MM-DD HH:MM:SS" without a zone in most responses
|
|
113
|
+
// (implicitly IST), but ISO-8601 with +0530 for historical candles.
|
|
114
|
+
const normalised = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value) ? `${value.replace(' ', 'T')}+05:30` : value;
|
|
115
|
+
const parsed = new Date(normalised);
|
|
116
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse a user-supplied date for historical queries.
|
|
120
|
+
*
|
|
121
|
+
* Accepts YYYY-MM-DD, "YYYY-MM-DD HH:MM:SS", and relative offsets like "7d",
|
|
122
|
+
* "3m", "1y" meaning "N units ago".
|
|
123
|
+
*/
|
|
124
|
+
export function parseUserDate(value, now = new Date()) {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
const relative = /^(\d+)\s*([dwmy])$/i.exec(trimmed);
|
|
127
|
+
if (relative) {
|
|
128
|
+
const amount = Number(relative[1]);
|
|
129
|
+
const unit = relative[2].toLowerCase();
|
|
130
|
+
const result = new Date(now);
|
|
131
|
+
if (unit === 'd')
|
|
132
|
+
result.setDate(result.getDate() - amount);
|
|
133
|
+
else if (unit === 'w')
|
|
134
|
+
result.setDate(result.getDate() - amount * 7);
|
|
135
|
+
else if (unit === 'm')
|
|
136
|
+
result.setMonth(result.getMonth() - amount);
|
|
137
|
+
else
|
|
138
|
+
result.setFullYear(result.getFullYear() - amount);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
if (trimmed.toLowerCase() === 'today') {
|
|
142
|
+
return now;
|
|
143
|
+
}
|
|
144
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
145
|
+
// Interpret a bare date as IST midnight, not UTC.
|
|
146
|
+
const parsed = new Date(`${trimmed}T00:00:00+05:30`);
|
|
147
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
148
|
+
}
|
|
149
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/.test(trimmed)) {
|
|
150
|
+
const withSeconds = trimmed.length === 16 ? `${trimmed}:00` : trimmed;
|
|
151
|
+
const parsed = new Date(`${withSeconds.replace(' ', 'T')}+05:30`);
|
|
152
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
153
|
+
}
|
|
154
|
+
const fallback = new Date(trimmed);
|
|
155
|
+
return Number.isNaN(fallback.getTime()) ? null : fallback;
|
|
156
|
+
}
|
|
157
|
+
/** Truncate to a display width, appending an ellipsis. */
|
|
158
|
+
export function truncate(value, max) {
|
|
159
|
+
if (max <= 1)
|
|
160
|
+
return value.slice(0, Math.max(0, max));
|
|
161
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
162
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output context.
|
|
3
|
+
*
|
|
4
|
+
* The contract, per https://clig.dev:
|
|
5
|
+
* - data goes to stdout, everything else (logs, progress, errors) to stderr
|
|
6
|
+
* - `--json` forces machine mode even on a TTY, because users eyeball output
|
|
7
|
+
* before piping it
|
|
8
|
+
* - stdout.isTTY and stderr.isTTY are checked INDEPENDENTLY; they differ
|
|
9
|
+
* constantly (`kite quote > file` leaves stderr a TTY)
|
|
10
|
+
*/
|
|
11
|
+
export interface IoStreams {
|
|
12
|
+
stdout: NodeJS.WritableStream;
|
|
13
|
+
stderr: NodeJS.WritableStream;
|
|
14
|
+
}
|
|
15
|
+
export interface IoOptions {
|
|
16
|
+
json?: boolean;
|
|
17
|
+
color?: 'auto' | 'always' | 'never';
|
|
18
|
+
quiet?: boolean;
|
|
19
|
+
streams?: IoStreams;
|
|
20
|
+
}
|
|
21
|
+
export declare class Io {
|
|
22
|
+
readonly json: boolean;
|
|
23
|
+
readonly quiet: boolean;
|
|
24
|
+
readonly stdoutIsTty: boolean;
|
|
25
|
+
readonly stderrIsTty: boolean;
|
|
26
|
+
readonly colorEnabled: boolean;
|
|
27
|
+
private readonly stdout;
|
|
28
|
+
private readonly stderr;
|
|
29
|
+
constructor(opts?: IoOptions);
|
|
30
|
+
/** True when the terminal can host a live-updating view. */
|
|
31
|
+
get interactive(): boolean;
|
|
32
|
+
/** Structured data. Always stdout. */
|
|
33
|
+
write(text: string): void;
|
|
34
|
+
line(text?: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Emit a JSON document. Values pass through the redactor first, so a stray
|
|
37
|
+
* token in an API payload cannot be printed or piped into a log.
|
|
38
|
+
*/
|
|
39
|
+
writeJson(value: unknown): void;
|
|
40
|
+
/** Human-facing notes. Always stderr, so piped stdout stays clean. */
|
|
41
|
+
note(text: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Write to stderr even under --quiet or --json.
|
|
44
|
+
*
|
|
45
|
+
* Reserved for output the user must see regardless of what they asked to
|
|
46
|
+
* suppress — specifically the order preview attached to a confirmation
|
|
47
|
+
* prompt. Being asked to approve an order with none of the facts shown is
|
|
48
|
+
* not informed consent, whatever the output mode.
|
|
49
|
+
*/
|
|
50
|
+
forceNote(text: string): void;
|
|
51
|
+
info(text: string): void;
|
|
52
|
+
success(text: string): void;
|
|
53
|
+
warn(text: string): void;
|
|
54
|
+
error(text: string): void;
|
|
55
|
+
private paint;
|
|
56
|
+
bold(text: string): string;
|
|
57
|
+
dim(text: string): string;
|
|
58
|
+
red(text: string): string;
|
|
59
|
+
green(text: string): string;
|
|
60
|
+
yellow(text: string): string;
|
|
61
|
+
blue(text: string): string;
|
|
62
|
+
cyan(text: string): string;
|
|
63
|
+
magenta(text: string): string;
|
|
64
|
+
gray(text: string): string;
|
|
65
|
+
/** Green for positive, red for negative, plain for zero. */
|
|
66
|
+
signed(value: number, text: string): string;
|
|
67
|
+
/** Terminal width, with a sane default when stdout is not a TTY. */
|
|
68
|
+
get columns(): number;
|
|
69
|
+
get rows(): number;
|
|
70
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { redact } from '../core/redact.js';
|
|
3
|
+
export class Io {
|
|
4
|
+
json;
|
|
5
|
+
quiet;
|
|
6
|
+
stdoutIsTty;
|
|
7
|
+
stderrIsTty;
|
|
8
|
+
colorEnabled;
|
|
9
|
+
stdout;
|
|
10
|
+
stderr;
|
|
11
|
+
constructor(opts = {}) {
|
|
12
|
+
this.stdout = opts.streams?.stdout ?? process.stdout;
|
|
13
|
+
this.stderr = opts.streams?.stderr ?? process.stderr;
|
|
14
|
+
this.stdoutIsTty = Boolean(this.stdout.isTTY);
|
|
15
|
+
this.stderrIsTty = Boolean(this.stderr.isTTY);
|
|
16
|
+
this.json = opts.json ?? false;
|
|
17
|
+
this.quiet = opts.quiet ?? false;
|
|
18
|
+
this.colorEnabled = resolveColor(opts.color ?? 'auto', this.stdoutIsTty, this.json);
|
|
19
|
+
}
|
|
20
|
+
/** True when the terminal can host a live-updating view. */
|
|
21
|
+
get interactive() {
|
|
22
|
+
return this.stdoutIsTty && !this.json;
|
|
23
|
+
}
|
|
24
|
+
/** Structured data. Always stdout. */
|
|
25
|
+
write(text) {
|
|
26
|
+
this.stdout.write(text);
|
|
27
|
+
}
|
|
28
|
+
line(text = '') {
|
|
29
|
+
this.stdout.write(`${text}\n`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Emit a JSON document. Values pass through the redactor first, so a stray
|
|
33
|
+
* token in an API payload cannot be printed or piped into a log.
|
|
34
|
+
*/
|
|
35
|
+
writeJson(value) {
|
|
36
|
+
this.stdout.write(`${JSON.stringify(redact(value), null, this.stdoutIsTty ? 2 : 0)}\n`);
|
|
37
|
+
}
|
|
38
|
+
/** Human-facing notes. Always stderr, so piped stdout stays clean. */
|
|
39
|
+
note(text) {
|
|
40
|
+
if (this.quiet || this.json)
|
|
41
|
+
return;
|
|
42
|
+
this.stderr.write(`${text}\n`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write to stderr even under --quiet or --json.
|
|
46
|
+
*
|
|
47
|
+
* Reserved for output the user must see regardless of what they asked to
|
|
48
|
+
* suppress — specifically the order preview attached to a confirmation
|
|
49
|
+
* prompt. Being asked to approve an order with none of the facts shown is
|
|
50
|
+
* not informed consent, whatever the output mode.
|
|
51
|
+
*/
|
|
52
|
+
forceNote(text) {
|
|
53
|
+
this.stderr.write(`${text}\n`);
|
|
54
|
+
}
|
|
55
|
+
info(text) {
|
|
56
|
+
this.note(`${this.dim('·')} ${text}`);
|
|
57
|
+
}
|
|
58
|
+
success(text) {
|
|
59
|
+
this.note(`${this.green('✓')} ${text}`);
|
|
60
|
+
}
|
|
61
|
+
warn(text) {
|
|
62
|
+
if (this.quiet)
|
|
63
|
+
return;
|
|
64
|
+
this.stderr.write(`${this.yellow('!')} ${text}\n`);
|
|
65
|
+
}
|
|
66
|
+
error(text) {
|
|
67
|
+
this.stderr.write(`${this.red('✗')} ${text}\n`);
|
|
68
|
+
}
|
|
69
|
+
// -- colour helpers, no-ops when colour is disabled ------------------------
|
|
70
|
+
paint(fn, text) {
|
|
71
|
+
return this.colorEnabled ? fn(text) : text;
|
|
72
|
+
}
|
|
73
|
+
bold(text) {
|
|
74
|
+
return this.paint(pc.bold, text);
|
|
75
|
+
}
|
|
76
|
+
dim(text) {
|
|
77
|
+
return this.paint(pc.dim, text);
|
|
78
|
+
}
|
|
79
|
+
red(text) {
|
|
80
|
+
return this.paint(pc.red, text);
|
|
81
|
+
}
|
|
82
|
+
green(text) {
|
|
83
|
+
return this.paint(pc.green, text);
|
|
84
|
+
}
|
|
85
|
+
yellow(text) {
|
|
86
|
+
return this.paint(pc.yellow, text);
|
|
87
|
+
}
|
|
88
|
+
blue(text) {
|
|
89
|
+
return this.paint(pc.blue, text);
|
|
90
|
+
}
|
|
91
|
+
cyan(text) {
|
|
92
|
+
return this.paint(pc.cyan, text);
|
|
93
|
+
}
|
|
94
|
+
magenta(text) {
|
|
95
|
+
return this.paint(pc.magenta, text);
|
|
96
|
+
}
|
|
97
|
+
gray(text) {
|
|
98
|
+
return this.paint(pc.gray, text);
|
|
99
|
+
}
|
|
100
|
+
/** Green for positive, red for negative, plain for zero. */
|
|
101
|
+
signed(value, text) {
|
|
102
|
+
if (value > 0)
|
|
103
|
+
return this.green(text);
|
|
104
|
+
if (value < 0)
|
|
105
|
+
return this.red(text);
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
/** Terminal width, with a sane default when stdout is not a TTY. */
|
|
109
|
+
get columns() {
|
|
110
|
+
const cols = this.stdout.columns;
|
|
111
|
+
if (typeof cols === 'number' && cols > 0)
|
|
112
|
+
return cols;
|
|
113
|
+
const fromEnv = Number(process.env['COLUMNS']);
|
|
114
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 80;
|
|
115
|
+
}
|
|
116
|
+
get rows() {
|
|
117
|
+
const rows = this.stdout.rows;
|
|
118
|
+
return typeof rows === 'number' && rows > 0 ? rows : 24;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve whether colour should be used.
|
|
123
|
+
*
|
|
124
|
+
* NO_COLOR is honoured whenever it is present and non-empty — per the spec it
|
|
125
|
+
* is not parsed for truthiness, so NO_COLOR=0 still disables colour.
|
|
126
|
+
*/
|
|
127
|
+
function resolveColor(mode, isTty, json) {
|
|
128
|
+
if (mode === 'never')
|
|
129
|
+
return false;
|
|
130
|
+
if (mode === 'always')
|
|
131
|
+
return true;
|
|
132
|
+
const noColor = process.env['NO_COLOR'];
|
|
133
|
+
if (noColor !== undefined && noColor !== '')
|
|
134
|
+
return false;
|
|
135
|
+
const forceColor = process.env['FORCE_COLOR'];
|
|
136
|
+
if (forceColor !== undefined && forceColor !== '' && forceColor !== '0')
|
|
137
|
+
return true;
|
|
138
|
+
if (process.env['TERM'] === 'dumb')
|
|
139
|
+
return false;
|
|
140
|
+
if (json)
|
|
141
|
+
return false;
|
|
142
|
+
return isTty;
|
|
143
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Io } from './io.js';
|
|
2
|
+
/**
|
|
3
|
+
* Table rendering.
|
|
4
|
+
*
|
|
5
|
+
* cli-table3 is used specifically because it measures column widths with
|
|
6
|
+
* `string-width`, which is ANSI- and East-Asian-width aware. That matters here:
|
|
7
|
+
* cells contain the ₹ symbol and colour escapes for P&L, and a naive width
|
|
8
|
+
* calculation misaligns every column below the first coloured row.
|
|
9
|
+
*/
|
|
10
|
+
export interface Column<T> {
|
|
11
|
+
header: string;
|
|
12
|
+
/** Rendered cell content. Colour is allowed; widths account for it. */
|
|
13
|
+
value: (row: T, io: Io) => string;
|
|
14
|
+
align?: 'left' | 'right' | 'center';
|
|
15
|
+
}
|
|
16
|
+
export interface TableOptions {
|
|
17
|
+
/** Omit borders and padding for a denser view. */
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
/** Shown instead of the table when there are no rows. */
|
|
20
|
+
empty?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function renderTable<T>(io: Io, rows: readonly T[], columns: Array<Column<T>>, opts?: TableOptions): string;
|
|
23
|
+
/** Print a table, or the equivalent JSON when --json is active. */
|
|
24
|
+
export declare function printTable<T>(io: Io, rows: readonly T[], columns: Array<Column<T>>, jsonValue: unknown, opts?: TableOptions): void;
|
|
25
|
+
/** A borderless key/value block, for detail views. */
|
|
26
|
+
export declare function renderKeyValue(io: Io, entries: Array<[string, string]>): string;
|
|
27
|
+
/** A section heading. */
|
|
28
|
+
export declare function heading(io: Io, text: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
export function renderTable(io, rows, columns, opts = {}) {
|
|
3
|
+
if (rows.length === 0) {
|
|
4
|
+
return io.dim(opts.empty ?? 'Nothing to show.');
|
|
5
|
+
}
|
|
6
|
+
const table = new Table({
|
|
7
|
+
head: columns.map((column) => io.bold(column.header)),
|
|
8
|
+
colAligns: columns.map((column) => column.align ?? 'left'),
|
|
9
|
+
style: {
|
|
10
|
+
head: [],
|
|
11
|
+
border: [],
|
|
12
|
+
compact: opts.compact ?? false,
|
|
13
|
+
'padding-left': 1,
|
|
14
|
+
'padding-right': 1,
|
|
15
|
+
},
|
|
16
|
+
chars: opts.compact ? COMPACT_CHARS : ROUNDED_CHARS,
|
|
17
|
+
});
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
table.push(columns.map((column) => column.value(row, io)));
|
|
20
|
+
}
|
|
21
|
+
return table.toString();
|
|
22
|
+
}
|
|
23
|
+
/** Print a table, or the equivalent JSON when --json is active. */
|
|
24
|
+
export function printTable(io, rows, columns, jsonValue, opts = {}) {
|
|
25
|
+
if (io.json) {
|
|
26
|
+
io.writeJson(jsonValue);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
io.line(renderTable(io, rows, columns, opts));
|
|
30
|
+
}
|
|
31
|
+
const ROUNDED_CHARS = {
|
|
32
|
+
top: '─',
|
|
33
|
+
'top-mid': '┬',
|
|
34
|
+
'top-left': '╭',
|
|
35
|
+
'top-right': '╮',
|
|
36
|
+
bottom: '─',
|
|
37
|
+
'bottom-mid': '┴',
|
|
38
|
+
'bottom-left': '╰',
|
|
39
|
+
'bottom-right': '╯',
|
|
40
|
+
left: '│',
|
|
41
|
+
'left-mid': '├',
|
|
42
|
+
mid: '─',
|
|
43
|
+
'mid-mid': '┼',
|
|
44
|
+
right: '│',
|
|
45
|
+
'right-mid': '┤',
|
|
46
|
+
middle: '│',
|
|
47
|
+
};
|
|
48
|
+
const COMPACT_CHARS = {
|
|
49
|
+
top: '',
|
|
50
|
+
'top-mid': '',
|
|
51
|
+
'top-left': '',
|
|
52
|
+
'top-right': '',
|
|
53
|
+
bottom: '',
|
|
54
|
+
'bottom-mid': '',
|
|
55
|
+
'bottom-left': '',
|
|
56
|
+
'bottom-right': '',
|
|
57
|
+
left: '',
|
|
58
|
+
'left-mid': '',
|
|
59
|
+
mid: '',
|
|
60
|
+
'mid-mid': '',
|
|
61
|
+
right: '',
|
|
62
|
+
'right-mid': '',
|
|
63
|
+
middle: ' ',
|
|
64
|
+
};
|
|
65
|
+
/** A borderless key/value block, for detail views. */
|
|
66
|
+
export function renderKeyValue(io, entries) {
|
|
67
|
+
const width = Math.max(...entries.map(([key]) => key.length));
|
|
68
|
+
return entries.map(([key, value]) => ` ${io.dim(key.padEnd(width))} ${value}`).join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** A section heading. */
|
|
71
|
+
export function heading(io, text) {
|
|
72
|
+
return `\n${io.bold(text)}`;
|
|
73
|
+
}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ExitCodeValue } from './core/errors.js';
|
|
2
|
+
import { type IoStreams } from './output/io.js';
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
argv?: string[];
|
|
5
|
+
streams?: IoStreams;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build and execute the CLI.
|
|
9
|
+
*
|
|
10
|
+
* Exported as a function taking argv and streams (rather than reading
|
|
11
|
+
* process.argv directly) so tests can drive it in-process. That matters
|
|
12
|
+
* because HTTP mocking cannot reach into a spawned child process — most tests
|
|
13
|
+
* call this, and only a thin smoke layer spawns the real binary.
|
|
14
|
+
*/
|
|
15
|
+
export declare function run(opts?: RunOptions): Promise<ExitCodeValue>;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Command, CommanderError } from 'commander';
|
|
2
|
+
import { createContext } from './context.js';
|
|
3
|
+
import { AbortedError, ExitCode, KiteCliError } from './core/errors.js';
|
|
4
|
+
import { redactString } from './core/redact.js';
|
|
5
|
+
import { Io } from './output/io.js';
|
|
6
|
+
const VERSION = '0.1.0';
|
|
7
|
+
/**
|
|
8
|
+
* Build and execute the CLI.
|
|
9
|
+
*
|
|
10
|
+
* Exported as a function taking argv and streams (rather than reading
|
|
11
|
+
* process.argv directly) so tests can drive it in-process. That matters
|
|
12
|
+
* because HTTP mocking cannot reach into a spawned child process — most tests
|
|
13
|
+
* call this, and only a thin smoke layer spawns the real binary.
|
|
14
|
+
*/
|
|
15
|
+
export async function run(opts = {}) {
|
|
16
|
+
const argv = opts.argv ?? process.argv;
|
|
17
|
+
// Handlers signal partial failure by setting process.exitCode (e.g. a sliced
|
|
18
|
+
// order where some legs succeeded). That is a process global, so reset it on
|
|
19
|
+
// entry — otherwise a second run() in the same process inherits the first
|
|
20
|
+
// run's failure. Matters for tests and for any embedder.
|
|
21
|
+
process.exitCode = undefined;
|
|
22
|
+
// Ctrl-C cancels in-flight requests rather than leaving them dangling.
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const onSigint = () => controller.abort(new AbortedError('Interrupted.'));
|
|
25
|
+
process.once('SIGINT', onSigint);
|
|
26
|
+
process.once('SIGTERM', onSigint);
|
|
27
|
+
const program = new Command();
|
|
28
|
+
let exitCode = ExitCode.Ok;
|
|
29
|
+
program
|
|
30
|
+
.name('kite')
|
|
31
|
+
.description('Unofficial command-line interface for the Zerodha Kite Connect API (not affiliated with Zerodha)')
|
|
32
|
+
.version(VERSION, '-v, --version')
|
|
33
|
+
.option('--json', 'Emit JSON instead of formatted tables')
|
|
34
|
+
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
35
|
+
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
36
|
+
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
37
|
+
.option('--quiet', 'Suppress informational messages')
|
|
38
|
+
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
39
|
+
.option('--env <env>', 'Environment: production or sandbox')
|
|
40
|
+
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
41
|
+
.option('--dry-run', 'Show what would happen without sending anything to Kite')
|
|
42
|
+
.showHelpAfterError('(run `kite --help` for usage)')
|
|
43
|
+
.configureOutput({
|
|
44
|
+
writeOut: (str) => (opts.streams?.stdout ?? process.stdout).write(str),
|
|
45
|
+
writeErr: (str) => (opts.streams?.stderr ?? process.stderr).write(str),
|
|
46
|
+
})
|
|
47
|
+
// Throw instead of calling process.exit, so `run()` stays testable and
|
|
48
|
+
// always returns its exit code.
|
|
49
|
+
.exitOverride();
|
|
50
|
+
/** Wraps a handler with context construction and error reporting. */
|
|
51
|
+
const withContext = (handler) => async (...args) => {
|
|
52
|
+
// Commander passes (…arguments, options, command).
|
|
53
|
+
const command = args[args.length - 1];
|
|
54
|
+
const options = args[args.length - 2];
|
|
55
|
+
const globals = program.opts();
|
|
56
|
+
const ctx = await createContext(globals, controller.signal, opts.streams);
|
|
57
|
+
try {
|
|
58
|
+
await handler(ctx, options, command);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
exitCode = reportError(err, ctx.io);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
// Registered lazily so a bare `kite quote` never pays to import the
|
|
65
|
+
// dashboard renderer, the ticker, or the prompt library.
|
|
66
|
+
const { authCommands } = await import('./commands/auth.js');
|
|
67
|
+
const { portfolioCommands } = await import('./commands/portfolio.js');
|
|
68
|
+
const { marketCommands } = await import('./commands/market.js');
|
|
69
|
+
const { orderCommands } = await import('./commands/orders.js');
|
|
70
|
+
const { gttCommands } = await import('./commands/gtt.js');
|
|
71
|
+
const { watchCommands } = await import('./commands/watch.js');
|
|
72
|
+
const { configCommands } = await import('./commands/config.js');
|
|
73
|
+
// commandsGroup applies to every command registered after it, so the group
|
|
74
|
+
// is set immediately before each block. With ~25 commands this is the
|
|
75
|
+
// difference between a readable --help and a wall of text.
|
|
76
|
+
program.commandsGroup('Account:');
|
|
77
|
+
authCommands(program, withContext);
|
|
78
|
+
program.commandsGroup('Portfolio:');
|
|
79
|
+
portfolioCommands(program, withContext);
|
|
80
|
+
program.commandsGroup('Market data:');
|
|
81
|
+
marketCommands(program, withContext);
|
|
82
|
+
program.commandsGroup('Trading:');
|
|
83
|
+
orderCommands(program, withContext);
|
|
84
|
+
gttCommands(program, withContext);
|
|
85
|
+
program.commandsGroup('Streaming:');
|
|
86
|
+
watchCommands(program, withContext);
|
|
87
|
+
program.commandsGroup('Settings:');
|
|
88
|
+
configCommands(program, withContext);
|
|
89
|
+
program.addHelpText('after', `
|
|
90
|
+
Examples:
|
|
91
|
+
$ kite login Authenticate and store a session
|
|
92
|
+
$ kite holdings Show your portfolio
|
|
93
|
+
$ kite quote NSE:INFY NSE:TCS Live quotes for two symbols
|
|
94
|
+
$ kite watch --holdings Stream your whole portfolio
|
|
95
|
+
$ kite history NSE:INFY -i 5minute --from 7d Recent 5-minute candles
|
|
96
|
+
$ kite orders place NSE:INFY -s BUY -q 1 --dry-run
|
|
97
|
+
Preview an order without sending it
|
|
98
|
+
$ kite positions --json | jq '.net[].pnl' Machine-readable output
|
|
99
|
+
|
|
100
|
+
Safety:
|
|
101
|
+
Order commands preview the resolved order and ask for confirmation.
|
|
102
|
+
Use --dry-run to validate without sending, or --yes to skip the prompt.
|
|
103
|
+
Disable trading entirely with: kite config set trading.enabled false
|
|
104
|
+
`);
|
|
105
|
+
try {
|
|
106
|
+
await program.parseAsync(argv);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err instanceof CommanderError) {
|
|
110
|
+
// --help and --version are reported as errors by exitOverride.
|
|
111
|
+
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.help' || err.code === 'commander.version') {
|
|
112
|
+
return ExitCode.Ok;
|
|
113
|
+
}
|
|
114
|
+
return ExitCode.Usage;
|
|
115
|
+
}
|
|
116
|
+
const io = new Io({ ...(opts.streams ? { streams: opts.streams } : {}) });
|
|
117
|
+
exitCode = reportError(err, io);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
process.removeListener('SIGINT', onSigint);
|
|
121
|
+
process.removeListener('SIGTERM', onSigint);
|
|
122
|
+
}
|
|
123
|
+
// A handler may have set process.exitCode directly (e.g. a partially failed
|
|
124
|
+
// sliced order), which should win over a clean return.
|
|
125
|
+
if (exitCode === ExitCode.Ok && typeof process.exitCode === 'number' && process.exitCode !== 0) {
|
|
126
|
+
return process.exitCode;
|
|
127
|
+
}
|
|
128
|
+
return exitCode;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Render an error and choose an exit code.
|
|
132
|
+
*
|
|
133
|
+
* Every message is redacted before printing: an API error can echo back input
|
|
134
|
+
* that contains a token, and a stack trace can contain the WebSocket URL.
|
|
135
|
+
*/
|
|
136
|
+
function reportError(err, io) {
|
|
137
|
+
if (err instanceof AbortedError) {
|
|
138
|
+
io.note('');
|
|
139
|
+
io.error(err.message);
|
|
140
|
+
return err.exitCode;
|
|
141
|
+
}
|
|
142
|
+
if (err instanceof KiteCliError) {
|
|
143
|
+
io.error(redactString(err.message));
|
|
144
|
+
if (err.hint)
|
|
145
|
+
io.note(` ${io.dim(err.hint)}`);
|
|
146
|
+
return err.exitCode;
|
|
147
|
+
}
|
|
148
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
149
|
+
io.error('Interrupted.');
|
|
150
|
+
return ExitCode.Aborted;
|
|
151
|
+
}
|
|
152
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
+
io.error(redactString(message));
|
|
154
|
+
if (err instanceof Error && err.stack && process.env['KITE_DEBUG_STACK'] === '1') {
|
|
155
|
+
io.note(redactString(err.stack));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
io.note(io.dim(' Set KITE_DEBUG_STACK=1 for a stack trace.'));
|
|
159
|
+
}
|
|
160
|
+
return ExitCode.Failure;
|
|
161
|
+
}
|