@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +292 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +7 -0
  5. package/dist/commands/auth.d.ts +2 -0
  6. package/dist/commands/auth.js +265 -0
  7. package/dist/commands/config.d.ts +2 -0
  8. package/dist/commands/config.js +182 -0
  9. package/dist/commands/gtt.d.ts +12 -0
  10. package/dist/commands/gtt.js +276 -0
  11. package/dist/commands/market.d.ts +2 -0
  12. package/dist/commands/market.js +389 -0
  13. package/dist/commands/orders.d.ts +2 -0
  14. package/dist/commands/orders.js +679 -0
  15. package/dist/commands/portfolio.d.ts +2 -0
  16. package/dist/commands/portfolio.js +286 -0
  17. package/dist/commands/types.d.ts +16 -0
  18. package/dist/commands/types.js +1 -0
  19. package/dist/commands/watch.d.ts +18 -0
  20. package/dist/commands/watch.js +285 -0
  21. package/dist/context.d.ts +40 -0
  22. package/dist/context.js +126 -0
  23. package/dist/core/api.d.ts +843 -0
  24. package/dist/core/api.js +472 -0
  25. package/dist/core/auth.d.ts +68 -0
  26. package/dist/core/auth.js +220 -0
  27. package/dist/core/client.d.ts +76 -0
  28. package/dist/core/client.js +285 -0
  29. package/dist/core/config.d.ts +114 -0
  30. package/dist/core/config.js +163 -0
  31. package/dist/core/credentials.d.ts +27 -0
  32. package/dist/core/credentials.js +182 -0
  33. package/dist/core/errors.d.ts +83 -0
  34. package/dist/core/errors.js +152 -0
  35. package/dist/core/instruments.d.ts +87 -0
  36. package/dist/core/instruments.js +288 -0
  37. package/dist/core/paths.d.ts +11 -0
  38. package/dist/core/paths.js +56 -0
  39. package/dist/core/ratelimit.d.ts +57 -0
  40. package/dist/core/ratelimit.js +196 -0
  41. package/dist/core/redact.d.ts +48 -0
  42. package/dist/core/redact.js +181 -0
  43. package/dist/core/schemas.d.ts +662 -0
  44. package/dist/core/schemas.js +433 -0
  45. package/dist/core/secretstore.d.ts +11 -0
  46. package/dist/core/secretstore.js +138 -0
  47. package/dist/core/session.d.ts +37 -0
  48. package/dist/core/session.js +102 -0
  49. package/dist/core/ticker.d.ts +131 -0
  50. package/dist/core/ticker.js +367 -0
  51. package/dist/index.d.ts +17 -0
  52. package/dist/index.js +17 -0
  53. package/dist/output/format.d.ts +31 -0
  54. package/dist/output/format.js +162 -0
  55. package/dist/output/io.d.ts +70 -0
  56. package/dist/output/io.js +143 -0
  57. package/dist/output/table.d.ts +28 -0
  58. package/dist/output/table.js +73 -0
  59. package/dist/run.d.ts +15 -0
  60. package/dist/run.js +161 -0
  61. package/dist/safety.d.ts +93 -0
  62. package/dist/safety.js +154 -0
  63. 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
+ }