@respan/cli 0.2.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 (100) hide show
  1. package/bin/dev.js +3 -0
  2. package/bin/run.js +10 -0
  3. package/dist/commands/auth/login.d.ts +14 -0
  4. package/dist/commands/auth/login.js +26 -0
  5. package/dist/commands/auth/logout.d.ts +12 -0
  6. package/dist/commands/auth/logout.js +18 -0
  7. package/dist/commands/auth/status.d.ts +12 -0
  8. package/dist/commands/auth/status.js +24 -0
  9. package/dist/commands/config/get.d.ts +15 -0
  10. package/dist/commands/config/get.js +15 -0
  11. package/dist/commands/config/list.d.ts +12 -0
  12. package/dist/commands/config/list.js +18 -0
  13. package/dist/commands/config/set.d.ts +16 -0
  14. package/dist/commands/config/set.js +18 -0
  15. package/dist/commands/datasets/add-spans.d.ts +16 -0
  16. package/dist/commands/datasets/add-spans.js +24 -0
  17. package/dist/commands/datasets/create-span.d.ts +16 -0
  18. package/dist/commands/datasets/create-span.js +36 -0
  19. package/dist/commands/datasets/create.d.ts +14 -0
  20. package/dist/commands/datasets/create.js +26 -0
  21. package/dist/commands/datasets/get-span.d.ts +16 -0
  22. package/dist/commands/datasets/get-span.js +23 -0
  23. package/dist/commands/datasets/get.d.ts +15 -0
  24. package/dist/commands/datasets/get.js +20 -0
  25. package/dist/commands/datasets/list.d.ts +14 -0
  26. package/dist/commands/datasets/list.js +23 -0
  27. package/dist/commands/datasets/spans.d.ts +15 -0
  28. package/dist/commands/datasets/spans.js +20 -0
  29. package/dist/commands/datasets/update.d.ts +17 -0
  30. package/dist/commands/datasets/update.js +32 -0
  31. package/dist/commands/evaluators/create.d.ts +16 -0
  32. package/dist/commands/evaluators/create.js +38 -0
  33. package/dist/commands/evaluators/get.d.ts +15 -0
  34. package/dist/commands/evaluators/get.js +20 -0
  35. package/dist/commands/evaluators/list.d.ts +14 -0
  36. package/dist/commands/evaluators/list.js +26 -0
  37. package/dist/commands/evaluators/run.d.ts +18 -0
  38. package/dist/commands/evaluators/run.js +41 -0
  39. package/dist/commands/evaluators/update.d.ts +18 -0
  40. package/dist/commands/evaluators/update.js +41 -0
  41. package/dist/commands/experiments/create.d.ts +16 -0
  42. package/dist/commands/experiments/create.js +39 -0
  43. package/dist/commands/experiments/get.d.ts +15 -0
  44. package/dist/commands/experiments/get.js +20 -0
  45. package/dist/commands/experiments/list.d.ts +14 -0
  46. package/dist/commands/experiments/list.js +23 -0
  47. package/dist/commands/logs/create.d.ts +16 -0
  48. package/dist/commands/logs/create.js +38 -0
  49. package/dist/commands/logs/get.d.ts +15 -0
  50. package/dist/commands/logs/get.js +20 -0
  51. package/dist/commands/logs/list.d.ts +18 -0
  52. package/dist/commands/logs/list.js +51 -0
  53. package/dist/commands/logs/summary.d.ts +14 -0
  54. package/dist/commands/logs/summary.js +23 -0
  55. package/dist/commands/prompts/create-version.d.ts +19 -0
  56. package/dist/commands/prompts/create-version.js +44 -0
  57. package/dist/commands/prompts/create.d.ts +14 -0
  58. package/dist/commands/prompts/create.js +26 -0
  59. package/dist/commands/prompts/get.d.ts +15 -0
  60. package/dist/commands/prompts/get.js +20 -0
  61. package/dist/commands/prompts/list.d.ts +13 -0
  62. package/dist/commands/prompts/list.js +22 -0
  63. package/dist/commands/prompts/update.d.ts +17 -0
  64. package/dist/commands/prompts/update.js +32 -0
  65. package/dist/commands/prompts/versions.d.ts +15 -0
  66. package/dist/commands/prompts/versions.js +20 -0
  67. package/dist/commands/traces/get.d.ts +15 -0
  68. package/dist/commands/traces/get.js +20 -0
  69. package/dist/commands/traces/list.d.ts +19 -0
  70. package/dist/commands/traces/list.js +47 -0
  71. package/dist/commands/traces/summary.d.ts +14 -0
  72. package/dist/commands/traces/summary.js +23 -0
  73. package/dist/commands/users/create.d.ts +16 -0
  74. package/dist/commands/users/create.js +38 -0
  75. package/dist/commands/users/get.d.ts +15 -0
  76. package/dist/commands/users/get.js +20 -0
  77. package/dist/commands/users/list.d.ts +16 -0
  78. package/dist/commands/users/list.js +38 -0
  79. package/dist/commands/users/update.d.ts +18 -0
  80. package/dist/commands/users/update.js +38 -0
  81. package/dist/commands/whoami.d.ts +12 -0
  82. package/dist/commands/whoami.js +21 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/index.js +1 -0
  85. package/dist/lib/auth.d.ts +16 -0
  86. package/dist/lib/auth.js +40 -0
  87. package/dist/lib/banner.d.ts +2 -0
  88. package/dist/lib/banner.js +117 -0
  89. package/dist/lib/base-command.d.ts +20 -0
  90. package/dist/lib/base-command.js +74 -0
  91. package/dist/lib/config.d.ts +26 -0
  92. package/dist/lib/config.js +81 -0
  93. package/dist/lib/output.d.ts +6 -0
  94. package/dist/lib/output.js +95 -0
  95. package/dist/lib/pagination.d.ts +12 -0
  96. package/dist/lib/pagination.js +34 -0
  97. package/dist/lib/spinner.d.ts +11 -0
  98. package/dist/lib/spinner.js +56 -0
  99. package/oclif.manifest.json +2886 -0
  100. package/package.json +52 -0
@@ -0,0 +1,21 @@
1
+ import { BaseCommand } from '../lib/base-command.js';
2
+ import { getActiveProfile, getCredential } from '../lib/config.js';
3
+ class Whoami extends BaseCommand {
4
+ async run() {
5
+ const { flags } = await this.parse(Whoami);
6
+ this.globalFlags = flags;
7
+ const profile = getActiveProfile();
8
+ const cred = getCredential(profile);
9
+ if (!cred) {
10
+ this.log('Not authenticated.');
11
+ return;
12
+ }
13
+ this.log(`Profile: ${profile}`);
14
+ if (cred.type === 'jwt')
15
+ this.log(`Email: ${cred.email}`);
16
+ this.log(`Base URL: ${cred.baseUrl}`);
17
+ }
18
+ }
19
+ Whoami.description = 'Show current user information';
20
+ Whoami.flags = { ...BaseCommand.baseFlags };
21
+ export default Whoami;
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,16 @@
1
+ import { Credential } from './config.js';
2
+ export interface AuthConfig {
3
+ apiKey?: string;
4
+ accessToken?: string;
5
+ refreshToken?: string;
6
+ baseUrl: string;
7
+ }
8
+ export declare function resolveAuth(flags: {
9
+ 'api-key'?: string;
10
+ profile?: string;
11
+ }): AuthConfig;
12
+ export declare function refreshJwtToken(credential: Credential & {
13
+ type: 'jwt';
14
+ }): Promise<{
15
+ access: string;
16
+ }>;
@@ -0,0 +1,40 @@
1
+ import { getCredential } from './config.js';
2
+ const DEFAULT_BASE_URL = 'https://api.respan.ai/api';
3
+ export function resolveAuth(flags) {
4
+ if (flags['api-key']) {
5
+ return { apiKey: flags['api-key'], baseUrl: DEFAULT_BASE_URL };
6
+ }
7
+ if (process.env.RESPAN_API_KEY) {
8
+ return {
9
+ apiKey: process.env.RESPAN_API_KEY,
10
+ baseUrl: process.env.RESPAN_API_BASE_URL || DEFAULT_BASE_URL,
11
+ };
12
+ }
13
+ const credential = getCredential(flags.profile);
14
+ if (credential) {
15
+ return credentialToAuth(credential);
16
+ }
17
+ throw new Error('Not authenticated. Run `respan auth login` or set RESPAN_API_KEY.');
18
+ }
19
+ function credentialToAuth(cred) {
20
+ if (cred.type === 'api_key') {
21
+ return { apiKey: cred.apiKey, baseUrl: cred.baseUrl };
22
+ }
23
+ return {
24
+ accessToken: cred.accessToken,
25
+ refreshToken: cred.refreshToken,
26
+ baseUrl: cred.baseUrl,
27
+ };
28
+ }
29
+ export async function refreshJwtToken(credential) {
30
+ const origin = credential.baseUrl.replace(/\/api\/?$/, '');
31
+ const response = await fetch(`${origin}/auth/jwt/refresh/`, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({ refresh: credential.refreshToken }),
35
+ });
36
+ if (!response.ok) {
37
+ throw new Error('Token refresh failed. Please login again with `respan auth login`.');
38
+ }
39
+ return response.json();
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function printBanner(): void;
2
+ export declare function printLoginSuccess(email?: string, profile?: string): Promise<void>;
@@ -0,0 +1,117 @@
1
+ const RESET = '\x1b[0m';
2
+ const GRADIENT_STOPS = [
3
+ [170, 190, 255], // #AABEFF bright
4
+ [100, 131, 240], // #6483F0 primary
5
+ [40, 65, 180], // #2841B4 deep
6
+ ];
7
+ function lerp(a, b, t) {
8
+ return Math.round(a + (b - a) * t);
9
+ }
10
+ function gradientColor(x, maxLen) {
11
+ const t = Math.min(x / Math.max(maxLen - 1, 1), 1);
12
+ const seg = t * (GRADIENT_STOPS.length - 1);
13
+ const idx = Math.min(Math.floor(seg), GRADIENT_STOPS.length - 2);
14
+ const lt = seg - idx;
15
+ const r = lerp(GRADIENT_STOPS[idx][0], GRADIENT_STOPS[idx + 1][0], lt);
16
+ const g = lerp(GRADIENT_STOPS[idx][1], GRADIENT_STOPS[idx + 1][1], lt);
17
+ const b = lerp(GRADIENT_STOPS[idx][2], GRADIENT_STOPS[idx + 1][2], lt);
18
+ return `\x1b[38;2;${r};${g};${b}m`;
19
+ }
20
+ // Layer 2: near black shadow
21
+ const SHADOW_COLOR = '\x1b[38;2;5;5;10m';
22
+ // Layer 3: gray shadow
23
+ const SHADOW_BG = '\x1b[38;2;177;179;188m';
24
+ // ASCII art from official Respan SVG logo: [.] Respan
25
+ // Half-blocks for correct aspect ratio, fits 80-col Apple Terminal
26
+ const BANNER_LINES = [
27
+ '▄████ █████ ▄▄▄▄▄▄',
28
+ '███ ███ ███▀████▄',
29
+ '███ ███ ███ ███ ▄███▄▄ ▄▄███▄ ██▄▄██▄▄ ▄▄███▄ ██▄▄███▄',
30
+ '███ ███ ███▄▄▄██▀ ██▀ ███ ██ ▀▀▀ ███ ▀██▄ ▀▀ ▀██ ███▀ ▀██',
31
+ '███ ███ ███▀▀██ █████████ ▀▀████▄ ██▄ ███ ▄██████ ███ ██',
32
+ '███ ██▄ ███ ███ ▀██ ██▄ ▄▄▄ ▄▄ ▄██ ███▄ ▄██▀███ ▄██ ███ ██',
33
+ '███▄▄ ▀ ▄▄███ ▀█▀ ▀██ ▀▀██▀▀ ▀▀███▀▀ ██▀▀██▀▀ ▀██▀▀██ ▀█▀ ▀█',
34
+ ' ▀▀▀▀ ▀▀▀▀▀ ██',
35
+ ' ▀▀',
36
+ ];
37
+ function renderBanner(lines) {
38
+ const grid = lines.map(l => [...l]);
39
+ const maxLen = Math.max(...grid.map(r => r.length));
40
+ for (const row of grid)
41
+ while (row.length < maxLen)
42
+ row.push(' ');
43
+ const H = grid.length, W = maxLen;
44
+ // Layer 2: offset (1, 0) — right edge shadow
45
+ const off2X = 1, off2Y = 0;
46
+ // Layer 3: offset (2, 1) — further right and down
47
+ const off3X = 2, off3Y = 1;
48
+ const outW = W + Math.max(off2X, off3X);
49
+ const outH = H + Math.max(off2Y, off3Y);
50
+ const result = [];
51
+ for (let y = 0; y < outH; y++) {
52
+ let line = '';
53
+ let lastColor = '';
54
+ for (let x = 0; x < outW; x++) {
55
+ const hasFg = y < H && x < W && grid[y][x] !== ' ';
56
+ const s2R = y - off2Y, s2C = x - off2X;
57
+ const hasL2 = s2R >= 0 && s2R < H && s2C >= 0 && s2C < W && grid[s2R][s2C] !== ' ';
58
+ const s3R = y - off3Y, s3C = x - off3X;
59
+ const hasL3 = s3R >= 0 && s3R < H && s3C >= 0 && s3C < W && grid[s3R][s3C] !== ' ';
60
+ if (hasFg) {
61
+ const c = gradientColor(x, maxLen);
62
+ if (c !== lastColor) {
63
+ line += c;
64
+ lastColor = c;
65
+ }
66
+ line += grid[y][x];
67
+ }
68
+ else if (hasL2) {
69
+ if (lastColor !== 'L2') {
70
+ line += SHADOW_COLOR;
71
+ lastColor = 'L2';
72
+ }
73
+ line += grid[s2R][s2C];
74
+ }
75
+ else if (hasL3) {
76
+ if (lastColor !== 'L3') {
77
+ line += SHADOW_BG;
78
+ lastColor = 'L3';
79
+ }
80
+ line += grid[s3R][s3C];
81
+ }
82
+ else {
83
+ line += ' ';
84
+ }
85
+ }
86
+ result.push(line.replace(/\s+$/, '') + RESET);
87
+ }
88
+ while (result.length && result[result.length - 1].replace(/\x1b\[[^m]*m/g, '').trim() === '')
89
+ result.pop();
90
+ return result;
91
+ }
92
+ const PC = '\x1b[38;2;100;131;240m'; // primary color
93
+ export function printBanner() {
94
+ if (!process.stdout.isTTY || process.env.NO_COLOR)
95
+ return;
96
+ console.log('');
97
+ for (const line of renderBanner(BANNER_LINES)) {
98
+ console.log(line);
99
+ }
100
+ console.log('');
101
+ }
102
+ export async function printLoginSuccess(email, profile) {
103
+ if (!process.stdout.isTTY || process.env.NO_COLOR) {
104
+ const msg = email ? `Logged in as ${email}.` : 'Logged in.';
105
+ const profileMsg = profile ? ` Profile "${profile}" saved.` : '';
106
+ console.log(`${msg}${profileMsg}`);
107
+ return;
108
+ }
109
+ console.log('');
110
+ if (email)
111
+ console.log(` ${PC}\u2713${RESET} Logged in as ${email}`);
112
+ else
113
+ console.log(` ${PC}\u2713${RESET} Logged in`);
114
+ if (profile)
115
+ console.log(` ${PC}\u2713${RESET} Profile "${profile}" saved`);
116
+ console.log('');
117
+ }
@@ -0,0 +1,20 @@
1
+ import { Command, Interfaces } from '@oclif/core';
2
+ import { RespanClient } from '@respan/respan-api';
3
+ import { AuthConfig } from './auth.js';
4
+ export type GlobalFlags = Interfaces.InferredFlags<typeof BaseCommand.baseFlags>;
5
+ export declare abstract class BaseCommand extends Command {
6
+ static baseFlags: {
7
+ 'api-key': Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
8
+ profile: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
9
+ json: Interfaces.BooleanFlag<boolean>;
10
+ csv: Interfaces.BooleanFlag<boolean>;
11
+ verbose: Interfaces.BooleanFlag<boolean>;
12
+ };
13
+ protected globalFlags: GlobalFlags;
14
+ protected getAuth(): AuthConfig;
15
+ protected getClient(): RespanClient;
16
+ protected getOutputFormat(): 'json' | 'csv' | 'table';
17
+ protected outputResult(data: unknown, columns?: string[]): void;
18
+ protected spin<T>(label: string, fn: () => Promise<T>): Promise<T>;
19
+ protected handleError(error: unknown): never;
20
+ }
@@ -0,0 +1,74 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { RespanClient } from '@respan/respan-api';
3
+ import { resolveAuth } from './auth.js';
4
+ import { outputData } from './output.js';
5
+ import { createSpinner } from './spinner.js';
6
+ export class BaseCommand extends Command {
7
+ getAuth() {
8
+ return resolveAuth({
9
+ 'api-key': this.globalFlags['api-key'],
10
+ profile: this.globalFlags.profile,
11
+ });
12
+ }
13
+ getClient() {
14
+ const auth = this.getAuth();
15
+ const token = auth.apiKey || auth.accessToken;
16
+ if (!token)
17
+ throw new Error('No API key or access token available.');
18
+ return new RespanClient({
19
+ token,
20
+ ...(auth.baseUrl ? { environment: auth.baseUrl } : {}),
21
+ });
22
+ }
23
+ getOutputFormat() {
24
+ if (this.globalFlags.json)
25
+ return 'json';
26
+ if (this.globalFlags.csv)
27
+ return 'csv';
28
+ return 'table';
29
+ }
30
+ outputResult(data, columns) {
31
+ this.log(outputData(data, this.getOutputFormat(), columns));
32
+ }
33
+ async spin(label, fn) {
34
+ const spinner = createSpinner(label);
35
+ spinner.start();
36
+ try {
37
+ const result = await fn();
38
+ spinner.succeed();
39
+ return result;
40
+ }
41
+ catch (error) {
42
+ spinner.fail();
43
+ throw error;
44
+ }
45
+ }
46
+ handleError(error) {
47
+ if (error instanceof Error) {
48
+ this.error(error.message, { exit: 1 });
49
+ }
50
+ this.error('An unexpected error occurred.', { exit: 1 });
51
+ }
52
+ }
53
+ BaseCommand.baseFlags = {
54
+ 'api-key': Flags.string({
55
+ description: 'API key (env: RESPAN_API_KEY)',
56
+ env: 'RESPAN_API_KEY',
57
+ }),
58
+ profile: Flags.string({
59
+ description: 'Named profile to use',
60
+ }),
61
+ json: Flags.boolean({
62
+ description: 'Output as JSON',
63
+ default: false,
64
+ }),
65
+ csv: Flags.boolean({
66
+ description: 'Output as CSV',
67
+ default: false,
68
+ }),
69
+ verbose: Flags.boolean({
70
+ char: 'v',
71
+ description: 'Show verbose output',
72
+ default: false,
73
+ }),
74
+ };
@@ -0,0 +1,26 @@
1
+ export interface ApiKeyCredential {
2
+ type: 'api_key';
3
+ apiKey: string;
4
+ baseUrl: string;
5
+ }
6
+ export interface JwtCredential {
7
+ type: 'jwt';
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ email: string;
11
+ baseUrl: string;
12
+ }
13
+ export type Credential = ApiKeyCredential | JwtCredential;
14
+ export interface Config {
15
+ activeProfile?: string;
16
+ defaults?: Record<string, string>;
17
+ }
18
+ export declare function getAllCredentials(): Record<string, Credential>;
19
+ export declare function getCredential(profile?: string): Credential | undefined;
20
+ export declare function setCredential(profile: string, credential: Credential): void;
21
+ export declare function deleteCredential(profile: string): void;
22
+ export declare function getConfig(): Config;
23
+ export declare function getActiveProfile(): string;
24
+ export declare function setActiveProfile(profile: string): void;
25
+ export declare function getConfigValue(key: string): string | undefined;
26
+ export declare function setConfigValue(key: string, value: string): void;
@@ -0,0 +1,81 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'respan');
5
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+ function ensureConfigDir() {
8
+ if (!fs.existsSync(CONFIG_DIR)) {
9
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
10
+ }
11
+ }
12
+ function readJson(filePath, fallback) {
13
+ try {
14
+ if (!fs.existsSync(filePath))
15
+ return fallback;
16
+ const raw = fs.readFileSync(filePath, 'utf-8');
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return fallback;
21
+ }
22
+ }
23
+ function writeJson(filePath, data) {
24
+ ensureConfigDir();
25
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
26
+ }
27
+ // --- Credentials ---
28
+ export function getAllCredentials() {
29
+ return readJson(CREDENTIALS_FILE, {});
30
+ }
31
+ export function getCredential(profile) {
32
+ const creds = getAllCredentials();
33
+ const name = profile || getActiveProfile();
34
+ return creds[name];
35
+ }
36
+ export function setCredential(profile, credential) {
37
+ const creds = getAllCredentials();
38
+ creds[profile] = credential;
39
+ writeJson(CREDENTIALS_FILE, creds);
40
+ // Set active profile if this is the first credential
41
+ const config = getConfig();
42
+ if (!config.activeProfile) {
43
+ setActiveProfile(profile);
44
+ }
45
+ }
46
+ export function deleteCredential(profile) {
47
+ const creds = getAllCredentials();
48
+ delete creds[profile];
49
+ writeJson(CREDENTIALS_FILE, creds);
50
+ }
51
+ // --- Config ---
52
+ export function getConfig() {
53
+ return readJson(CONFIG_FILE, {});
54
+ }
55
+ export function getActiveProfile() {
56
+ const config = getConfig();
57
+ return config.activeProfile || 'default';
58
+ }
59
+ export function setActiveProfile(profile) {
60
+ const config = getConfig();
61
+ config.activeProfile = profile;
62
+ writeJson(CONFIG_FILE, config);
63
+ }
64
+ export function getConfigValue(key) {
65
+ const config = getConfig();
66
+ if (key === 'activeProfile')
67
+ return config.activeProfile;
68
+ return config.defaults?.[key];
69
+ }
70
+ export function setConfigValue(key, value) {
71
+ const config = getConfig();
72
+ if (key === 'activeProfile') {
73
+ config.activeProfile = value;
74
+ }
75
+ else {
76
+ if (!config.defaults)
77
+ config.defaults = {};
78
+ config.defaults[key] = value;
79
+ }
80
+ writeJson(CONFIG_FILE, config);
81
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Output formatting utilities for the CLI.
3
+ * Supports JSON, CSV, and table output formats.
4
+ */
5
+ export type OutputFormat = 'json' | 'csv' | 'table';
6
+ export declare function outputData(data: unknown, format: OutputFormat, columns?: string[]): string;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Output formatting utilities for the CLI.
3
+ * Supports JSON, CSV, and table output formats.
4
+ */
5
+ export function outputData(data, format, columns) {
6
+ switch (format) {
7
+ case 'json':
8
+ return formatJson(data);
9
+ case 'csv':
10
+ return formatCsv(data, columns);
11
+ case 'table':
12
+ default:
13
+ return formatTable(data, columns);
14
+ }
15
+ }
16
+ function formatJson(data) {
17
+ return JSON.stringify(data, null, 2);
18
+ }
19
+ function formatCsv(data, columns) {
20
+ const rows = normalizeToArray(data);
21
+ if (rows.length === 0)
22
+ return '';
23
+ const cols = columns || Object.keys(rows[0]);
24
+ const header = cols.join(',');
25
+ const body = rows
26
+ .map((row) => {
27
+ const record = row;
28
+ return cols.map((col) => escapeCsvValue(record[col])).join(',');
29
+ })
30
+ .join('\n');
31
+ return `${header}\n${body}`;
32
+ }
33
+ function escapeCsvValue(value) {
34
+ if (value === null || value === undefined)
35
+ return '';
36
+ const str = String(value);
37
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
38
+ return `"${str.replace(/"/g, '""')}"`;
39
+ }
40
+ return str;
41
+ }
42
+ function formatTable(data, columns) {
43
+ const rows = normalizeToArray(data);
44
+ if (rows.length === 0)
45
+ return 'No data.';
46
+ const cols = columns || Object.keys(rows[0]);
47
+ const widths = {};
48
+ for (const col of cols) {
49
+ widths[col] = col.length;
50
+ }
51
+ const stringRows = rows.map((row) => {
52
+ const record = row;
53
+ const stringRow = {};
54
+ for (const col of cols) {
55
+ const val = truncate(formatValue(record[col]), 40);
56
+ stringRow[col] = val;
57
+ widths[col] = Math.max(widths[col], val.length);
58
+ }
59
+ return stringRow;
60
+ });
61
+ const header = cols.map((col) => col.padEnd(widths[col])).join(' ');
62
+ const separator = cols.map((col) => '-'.repeat(widths[col])).join(' ');
63
+ const body = stringRows
64
+ .map((row) => cols.map((col) => (row[col] || '').padEnd(widths[col])).join(' '))
65
+ .join('\n');
66
+ return `${header}\n${separator}\n${body}`;
67
+ }
68
+ function normalizeToArray(data) {
69
+ if (Array.isArray(data))
70
+ return data;
71
+ if (data && typeof data === 'object') {
72
+ const obj = data;
73
+ // Handle paginated responses
74
+ if ('results' in obj && Array.isArray(obj.results))
75
+ return obj.results;
76
+ if ('data' in obj && Array.isArray(obj.data))
77
+ return obj.data;
78
+ if ('items' in obj && Array.isArray(obj.items))
79
+ return obj.items;
80
+ return [data];
81
+ }
82
+ return [{ value: data }];
83
+ }
84
+ function formatValue(value) {
85
+ if (value === null || value === undefined)
86
+ return '-';
87
+ if (typeof value === 'object')
88
+ return JSON.stringify(value);
89
+ return String(value);
90
+ }
91
+ function truncate(str, maxLen) {
92
+ if (str.length <= maxLen)
93
+ return str;
94
+ return str.slice(0, maxLen - 3) + '...';
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pagination utilities for CLI list commands.
3
+ */
4
+ export interface PaginationInfo {
5
+ page: number;
6
+ totalPages?: number;
7
+ totalCount?: number;
8
+ hasNext: boolean;
9
+ hasPrevious: boolean;
10
+ }
11
+ export declare function extractPagination(response: unknown, currentPage: number): PaginationInfo;
12
+ export declare function formatPaginationInfo(info: PaginationInfo): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pagination utilities for CLI list commands.
3
+ */
4
+ export function extractPagination(response, currentPage) {
5
+ if (!response || typeof response !== 'object') {
6
+ return { page: currentPage, hasNext: false, hasPrevious: currentPage > 1 };
7
+ }
8
+ const obj = response;
9
+ const count = typeof obj.count === 'number' ? obj.count : undefined;
10
+ const next = obj.next != null;
11
+ const previous = obj.previous != null;
12
+ const totalPages = typeof obj.total_pages === 'number'
13
+ ? obj.total_pages
14
+ : count && typeof obj.page_size === 'number'
15
+ ? Math.ceil(count / obj.page_size)
16
+ : undefined;
17
+ return {
18
+ page: currentPage,
19
+ totalPages,
20
+ totalCount: count,
21
+ hasNext: next,
22
+ hasPrevious: previous,
23
+ };
24
+ }
25
+ export function formatPaginationInfo(info) {
26
+ const parts = [`Page ${info.page}`];
27
+ if (info.totalPages)
28
+ parts[0] += ` of ${info.totalPages}`;
29
+ if (info.totalCount !== undefined)
30
+ parts.push(`Total: ${info.totalCount}`);
31
+ if (info.hasNext)
32
+ parts.push('(more results available)');
33
+ return parts.join(' | ');
34
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Simple TTY spinner for CLI feedback.
3
+ * Falls back to plain text when not in a TTY or NO_COLOR is set.
4
+ */
5
+ export interface Spinner {
6
+ start(): void;
7
+ succeed(text?: string): void;
8
+ fail(text?: string): void;
9
+ stop(): void;
10
+ }
11
+ export declare function createSpinner(label: string): Spinner;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Simple TTY spinner for CLI feedback.
3
+ * Falls back to plain text when not in a TTY or NO_COLOR is set.
4
+ */
5
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ const INTERVAL = 80;
7
+ export function createSpinner(label) {
8
+ const isTTY = process.stderr.isTTY && !process.env.NO_COLOR;
9
+ if (!isTTY) {
10
+ return {
11
+ start() {
12
+ process.stderr.write(`${label}...\n`);
13
+ },
14
+ succeed(text) {
15
+ process.stderr.write(`${text || label} done.\n`);
16
+ },
17
+ fail(text) {
18
+ process.stderr.write(`${text || label} failed.\n`);
19
+ },
20
+ stop() { },
21
+ };
22
+ }
23
+ let frameIndex = 0;
24
+ let timer = null;
25
+ function clear() {
26
+ process.stderr.write('\r\x1b[K');
27
+ }
28
+ function render() {
29
+ clear();
30
+ process.stderr.write(`${FRAMES[frameIndex]} ${label}`);
31
+ frameIndex = (frameIndex + 1) % FRAMES.length;
32
+ }
33
+ return {
34
+ start() {
35
+ render();
36
+ timer = setInterval(render, INTERVAL);
37
+ },
38
+ succeed(text) {
39
+ if (timer)
40
+ clearInterval(timer);
41
+ clear();
42
+ process.stderr.write(`\x1b[32m✓\x1b[0m ${text || label}\n`);
43
+ },
44
+ fail(text) {
45
+ if (timer)
46
+ clearInterval(timer);
47
+ clear();
48
+ process.stderr.write(`\x1b[31m✗\x1b[0m ${text || label}\n`);
49
+ },
50
+ stop() {
51
+ if (timer)
52
+ clearInterval(timer);
53
+ clear();
54
+ },
55
+ };
56
+ }