@burdenoff/bigconsole-cli 2026.802.1
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 +9 -0
- package/README.md +235 -0
- package/dist/auth/device-code.d.ts +66 -0
- package/dist/auth/device-code.js +239 -0
- package/dist/auth/store.d.ts +46 -0
- package/dist/auth/store.js +90 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +126 -0
- package/dist/commands/auth.d.ts +5 -0
- package/dist/commands/auth.js +352 -0
- package/dist/commands/base.d.ts +47 -0
- package/dist/commands/base.js +100 -0
- package/dist/commands/cli-config.d.ts +6 -0
- package/dist/commands/cli-config.js +79 -0
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +220 -0
- package/dist/commands/datasink.d.ts +5 -0
- package/dist/commands/datasink.js +175 -0
- package/dist/commands/drilldown.d.ts +9 -0
- package/dist/commands/drilldown.js +47 -0
- package/dist/commands/index.d.ts +13 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/parser.d.ts +5 -0
- package/dist/commands/parser.js +181 -0
- package/dist/commands/pipeline.d.ts +5 -0
- package/dist/commands/pipeline.js +142 -0
- package/dist/commands/prompt.d.ts +5 -0
- package/dist/commands/prompt.js +84 -0
- package/dist/commands/share.d.ts +8 -0
- package/dist/commands/share.js +50 -0
- package/dist/commands/template.d.ts +5 -0
- package/dist/commands/template.js +62 -0
- package/dist/commands/widget.d.ts +5 -0
- package/dist/commands/widget.js +164 -0
- package/dist/commands/workflow.d.ts +9 -0
- package/dist/commands/workflow.js +54 -0
- package/dist/commands/workspace.d.ts +9 -0
- package/dist/commands/workspace.js +95 -0
- package/dist/config/index.d.ts +72 -0
- package/dist/config/index.js +151 -0
- package/dist/graphql/client.d.ts +26 -0
- package/dist/graphql/client.js +274 -0
- package/dist/graphql/operations.d.ts +48 -0
- package/dist/graphql/operations.js +654 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/output.d.ts +62 -0
- package/dist/utils/output.js +186 -0
- package/package.json +116 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BigConsole CLI — dashboard, widget, parser, and workflow management from the terminal.
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import boxen from 'boxen';
|
|
8
|
+
import updateNotifier from 'update-notifier';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { registerAuthCommands, registerCLIConfigCommands, registerWorkspaceCommands, registerDashboardCommands, registerWidgetCommands, registerDataSinkCommands, registerParserCommands, registerPipelineCommands, registerWorkflowCommands, registerDrilldownCommands, registerShareCommands, registerTemplateCommands, registerPromptCommands, } from './commands/index.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
let pkg = {
|
|
16
|
+
name: '@burdenoff/bigconsole-cli',
|
|
17
|
+
version: '0.0.0-dev',
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
const packagePath = join(__dirname, '../package.json');
|
|
21
|
+
pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// package.json not found in dev mode — use defaults
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const notifier = updateNotifier({
|
|
28
|
+
pkg,
|
|
29
|
+
updateCheckInterval: 1000 * 60 * 60 * 24,
|
|
30
|
+
});
|
|
31
|
+
// Suppress the update banner when the caller asked for machine-readable
|
|
32
|
+
// output (`--format json|yaml`) or when stdout isn't a TTY. Printing a
|
|
33
|
+
// human-readable box on stdout corrupts JSON/YAML piped to jq, yq,
|
|
34
|
+
// shell vars, etc. Also send the banner to stderr (not stdout) so
|
|
35
|
+
// it still doesn't mingle with data even in formats we miss.
|
|
36
|
+
const argv = process.argv.slice(2);
|
|
37
|
+
const fmtIndex = argv.findIndex((a) => a === '-f' || a === '--format');
|
|
38
|
+
const fmt = fmtIndex !== -1 ? argv[fmtIndex + 1]?.toLowerCase() : undefined;
|
|
39
|
+
const isMachineOutput = fmt === 'json' || fmt === 'yaml' || fmt === 'yml';
|
|
40
|
+
const isTty = Boolean(process.stdout.isTTY);
|
|
41
|
+
if (notifier.update && !isMachineOutput && isTty) {
|
|
42
|
+
const message = `Update available: ${chalk.dim(notifier.update.current)} → ${chalk.green(notifier.update.latest)}\nRun ${chalk.cyan(`bun add -g ${pkg.name}`)} to update`;
|
|
43
|
+
process.stderr.write(boxen(message, {
|
|
44
|
+
padding: 1,
|
|
45
|
+
margin: 1,
|
|
46
|
+
align: 'center',
|
|
47
|
+
borderColor: 'yellow',
|
|
48
|
+
borderStyle: 'round',
|
|
49
|
+
}) + '\n');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Update check is best-effort; never block CLI startup on a network failure.
|
|
54
|
+
}
|
|
55
|
+
const program = new Command();
|
|
56
|
+
program
|
|
57
|
+
.name('bigconsole')
|
|
58
|
+
.description('BigConsole CLI — dashboards, widgets, parsers, and workflows from the terminal')
|
|
59
|
+
.version(pkg.version, '-v, --version')
|
|
60
|
+
.option('-f, --format <format>', 'Output format (json, table, yaml)')
|
|
61
|
+
.option('--verbose', 'Enable verbose logging')
|
|
62
|
+
.option('--no-color', 'Disable colored output');
|
|
63
|
+
registerAuthCommands(program);
|
|
64
|
+
registerCLIConfigCommands(program);
|
|
65
|
+
registerWorkspaceCommands(program);
|
|
66
|
+
registerDashboardCommands(program);
|
|
67
|
+
registerWidgetCommands(program);
|
|
68
|
+
registerDataSinkCommands(program);
|
|
69
|
+
registerParserCommands(program);
|
|
70
|
+
registerPipelineCommands(program);
|
|
71
|
+
registerWorkflowCommands(program);
|
|
72
|
+
registerDrilldownCommands(program);
|
|
73
|
+
registerShareCommands(program);
|
|
74
|
+
registerTemplateCommands(program);
|
|
75
|
+
registerPromptCommands(program);
|
|
76
|
+
program.addHelpText('after', `
|
|
77
|
+
${chalk.bold('Examples:')}
|
|
78
|
+
${chalk.dim('# Authentication')}
|
|
79
|
+
$ bigconsole auth login ${chalk.dim('# OAuth2 Device Code login')}
|
|
80
|
+
$ bigconsole auth status ${chalk.dim('# Show auth state')}
|
|
81
|
+
|
|
82
|
+
${chalk.dim('# Workspace selection')}
|
|
83
|
+
$ bigconsole workspace list ${chalk.dim('# List accessible workspaces')}
|
|
84
|
+
$ bigconsole workspace set <id> ${chalk.dim('# Switch active workspace')}
|
|
85
|
+
|
|
86
|
+
${chalk.dim('# Core domain')}
|
|
87
|
+
$ bigconsole dashboard list --search sales ${chalk.dim('# List dashboards')}
|
|
88
|
+
$ bigconsole dashboard get <id> ${chalk.dim('# Inspect a dashboard')}
|
|
89
|
+
$ bigconsole widget list --page <pageId> ${chalk.dim('# Widgets on a page')}
|
|
90
|
+
$ bigconsole datasink list ${chalk.dim('# List data sinks')}
|
|
91
|
+
$ bigconsole datasink data --key orders --force-refresh ${chalk.dim('# Fetch live payload')}
|
|
92
|
+
$ bigconsole parser list ${chalk.dim('# List parsers')}
|
|
93
|
+
$ bigconsole pipeline list ${chalk.dim('# List pipelines')}
|
|
94
|
+
$ bigconsole workflow list ${chalk.dim('# List workflows')}
|
|
95
|
+
|
|
96
|
+
${chalk.dim('# CLI config')}
|
|
97
|
+
$ bigconsole cli-config show
|
|
98
|
+
$ bigconsole cli-config set workspaceEndpoint https://graphqlworkspaces.burdenoff.com/workspaces/graphql
|
|
99
|
+
|
|
100
|
+
${chalk.bold('Command groups:')}
|
|
101
|
+
${chalk.cyan('auth')} login, logout, status, token, refresh
|
|
102
|
+
${chalk.cyan('workspace')} list, current, set
|
|
103
|
+
${chalk.cyan('dashboard')} list, get, create, update, archive, delete, import, export
|
|
104
|
+
${chalk.cyan('widget')} list, get, create, update, delete
|
|
105
|
+
${chalk.cyan('datasink')} list, get, data, create, update, delete
|
|
106
|
+
${chalk.cyan('parser')} list, get, create, update, delete
|
|
107
|
+
${chalk.cyan('pipeline')} list, get, create, update, delete
|
|
108
|
+
${chalk.cyan('workflow')} list, get
|
|
109
|
+
${chalk.cyan('drilldown')} list, delete
|
|
110
|
+
${chalk.cyan('share')} list
|
|
111
|
+
${chalk.cyan('template')} list, get
|
|
112
|
+
${chalk.cyan('prompt')} generate, list, get, cancel
|
|
113
|
+
${chalk.cyan('cli-config')} show, get, set, path, reset
|
|
114
|
+
|
|
115
|
+
${chalk.bold('Configuration:')} ${chalk.dim('~/.config/bigconsole-cli/')}
|
|
116
|
+
${chalk.bold('Docs:')} ${chalk.blue('https://docs.bigconsole.com/cli')}
|
|
117
|
+
`);
|
|
118
|
+
program.on('command:*', () => {
|
|
119
|
+
console.error(chalk.red(`Invalid command: ${program.args.join(' ')}`));
|
|
120
|
+
console.log(chalk.dim('See --help for a list of available commands.'));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
program.parse();
|
|
124
|
+
if (!process.argv.slice(2).length) {
|
|
125
|
+
program.outputHelp();
|
|
126
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth commands: login, logout, status, token, refresh.
|
|
3
|
+
*/
|
|
4
|
+
import { readConfig, resolveOIDCConfig } from '../config/index.js';
|
|
5
|
+
import { readAuth, writeAuth, clearAuth, isTokenExpired } from '../auth/store.js';
|
|
6
|
+
import { startDeviceCodeFlow, pollForToken, openBrowser, clientCredentialsFlow, issueWorkspaceToken, fetchWorkspaces, } from '../auth/device-code.js';
|
|
7
|
+
import { globalRequest, resetClients } from '../graphql/client.js';
|
|
8
|
+
import { SIGN_OUT, REFRESH_TOKEN } from '../graphql/operations.js';
|
|
9
|
+
import { printSuccess, printError, printInfo, printWarning, formatDate } from '../utils/output.js';
|
|
10
|
+
import { handleCommandError, formatAndOutput } from './base.js';
|
|
11
|
+
export function registerAuthCommands(program) {
|
|
12
|
+
const auth = program.command('auth').description('Authentication commands');
|
|
13
|
+
auth
|
|
14
|
+
.command('login')
|
|
15
|
+
.description('Authenticate with BigConsole')
|
|
16
|
+
.option('--client-credentials', 'Authenticate as an app with client ID and secret')
|
|
17
|
+
.option('--workspace-id <id>', 'Workspace ID (for app auth)')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
if (options.clientCredentials) {
|
|
21
|
+
await loginWithClientCredentials(options.workspaceId);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await loginWithDeviceCode();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
handleCommandError(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
auth
|
|
32
|
+
.command('logout')
|
|
33
|
+
.description('Revoke tokens and clear stored auth')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const authData = readAuth();
|
|
37
|
+
if (authData?.accessToken) {
|
|
38
|
+
try {
|
|
39
|
+
await globalRequest(SIGN_OUT);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
printWarning('Server-side logout failed, clearing local tokens');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
clearAuth();
|
|
46
|
+
resetClients();
|
|
47
|
+
printSuccess('Logged out successfully');
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
handleCommandError(error);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
auth
|
|
54
|
+
.command('status')
|
|
55
|
+
.description('Show current auth state')
|
|
56
|
+
.option('-f, --format <format>', 'Output format (json, table)')
|
|
57
|
+
.action((options) => {
|
|
58
|
+
const authData = readAuth();
|
|
59
|
+
const config = readConfig();
|
|
60
|
+
const authenticated = Boolean(authData?.accessToken);
|
|
61
|
+
const expired = authData?.accessToken ? isTokenExpired(authData) : false;
|
|
62
|
+
const expiresAt = authData?.expiresAt
|
|
63
|
+
? formatDate(new Date(authData.expiresAt).toISOString())
|
|
64
|
+
: '-';
|
|
65
|
+
const method = authData?.authMethod === 'client_credentials' ? 'Client Credentials' : 'OAuth2 Device Code';
|
|
66
|
+
const wsTokenExpires = authData?.workspaceTokenExpiresAt
|
|
67
|
+
? formatDate(new Date(authData.workspaceTokenExpiresAt).toISOString())
|
|
68
|
+
: '-';
|
|
69
|
+
formatAndOutput(options, {
|
|
70
|
+
authenticated,
|
|
71
|
+
method,
|
|
72
|
+
expired,
|
|
73
|
+
expiresAt,
|
|
74
|
+
workspaceId: authData?.workspaceId ?? '-',
|
|
75
|
+
organizationId: authData?.organizationId ?? '-',
|
|
76
|
+
workspaceTokenExpiresAt: wsTokenExpires,
|
|
77
|
+
workspaceEndpoint: config.workspaceEndpoint,
|
|
78
|
+
globalEndpoint: config.globalEndpoint,
|
|
79
|
+
}, {
|
|
80
|
+
headers: ['Key', 'Value'],
|
|
81
|
+
rows: [
|
|
82
|
+
['Authenticated', String(authenticated)],
|
|
83
|
+
['Method', method],
|
|
84
|
+
['Expired', String(expired)],
|
|
85
|
+
['Expires At', expiresAt],
|
|
86
|
+
['Workspace ID', authData?.workspaceId ?? '-'],
|
|
87
|
+
['Organization ID', authData?.organizationId ?? '-'],
|
|
88
|
+
['Workspace Token Expires', wsTokenExpires],
|
|
89
|
+
['Workspace Endpoint', config.workspaceEndpoint],
|
|
90
|
+
['Global Endpoint', config.globalEndpoint],
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
auth
|
|
95
|
+
.command('token')
|
|
96
|
+
.description('Print access token to stdout')
|
|
97
|
+
.option('--workspace', 'Print workspace token instead')
|
|
98
|
+
.action((options) => {
|
|
99
|
+
const authData = readAuth();
|
|
100
|
+
if (options.workspace) {
|
|
101
|
+
if (!authData?.workspaceToken) {
|
|
102
|
+
printError('No workspace token. Log in and select a workspace: bigconsole auth login');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(authData.workspaceToken);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!authData?.accessToken) {
|
|
109
|
+
printError('Not authenticated. Run: bigconsole auth login');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
process.stdout.write(authData.accessToken);
|
|
113
|
+
});
|
|
114
|
+
auth
|
|
115
|
+
.command('refresh')
|
|
116
|
+
.description('Force token refresh')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
try {
|
|
119
|
+
const authData = readAuth();
|
|
120
|
+
if (!authData?.refreshToken && authData?.authMethod !== 'client_credentials') {
|
|
121
|
+
printError('No refresh token available. Run: bigconsole auth login');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (authData?.authMethod === 'client_credentials' &&
|
|
125
|
+
authData.clientId &&
|
|
126
|
+
authData.clientSecret) {
|
|
127
|
+
const config = readConfig();
|
|
128
|
+
const result = await clientCredentialsFlow(config.globalEndpoint, authData.clientId, authData.clientSecret);
|
|
129
|
+
writeAuth({
|
|
130
|
+
...authData,
|
|
131
|
+
accessToken: result.accessToken,
|
|
132
|
+
expiresAt: Date.now() + result.expiresIn * 1000,
|
|
133
|
+
});
|
|
134
|
+
resetClients();
|
|
135
|
+
printSuccess('App token refreshed successfully');
|
|
136
|
+
if (authData.workspaceId && authData.organizationId) {
|
|
137
|
+
await refreshWorkspaceTokenForAuth();
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const data = await globalRequest(REFRESH_TOKEN, {
|
|
142
|
+
token: authData.refreshToken,
|
|
143
|
+
});
|
|
144
|
+
const r = data.refreshToken;
|
|
145
|
+
writeAuth({
|
|
146
|
+
...authData,
|
|
147
|
+
accessToken: r.accessToken,
|
|
148
|
+
refreshToken: r.refreshToken,
|
|
149
|
+
expiresAt: Date.now() + r.expiresIn * 1000,
|
|
150
|
+
});
|
|
151
|
+
resetClients();
|
|
152
|
+
printSuccess('Token refreshed successfully');
|
|
153
|
+
if (authData.workspaceId && authData.organizationId) {
|
|
154
|
+
await refreshWorkspaceTokenForAuth();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
handleCommandError(error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function refreshWorkspaceTokenForAuth() {
|
|
163
|
+
const authData = readAuth();
|
|
164
|
+
if (!authData?.accessToken || !authData.workspaceId || !authData.organizationId)
|
|
165
|
+
return;
|
|
166
|
+
const config = readConfig();
|
|
167
|
+
try {
|
|
168
|
+
const wsResult = await issueWorkspaceToken(config.workspaceEndpoint, authData.accessToken, authData.workspaceId, authData.organizationId);
|
|
169
|
+
writeAuth({
|
|
170
|
+
...authData,
|
|
171
|
+
workspaceToken: wsResult.token,
|
|
172
|
+
workspaceTokenExpiresAt: new Date(wsResult.expiresAt).getTime(),
|
|
173
|
+
});
|
|
174
|
+
resetClients();
|
|
175
|
+
printSuccess('Workspace token refreshed');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
printWarning('Workspace token refresh failed — workspace operations may require re-login');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function loginWithDeviceCode() {
|
|
182
|
+
const config = readConfig();
|
|
183
|
+
const { oidcIssuer, clientId } = resolveOIDCConfig();
|
|
184
|
+
const oidcConfig = { ...config, oidcIssuer, clientId };
|
|
185
|
+
printInfo('Starting device code authentication...');
|
|
186
|
+
const dc = await startDeviceCodeFlow(oidcConfig);
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(` Open: ${dc.verificationUri}`);
|
|
189
|
+
console.log(` Code: ${dc.userCode}`);
|
|
190
|
+
console.log('');
|
|
191
|
+
const opened = await openBrowser(dc.verificationUriComplete ?? dc.verificationUri);
|
|
192
|
+
if (opened)
|
|
193
|
+
printInfo('Browser opened');
|
|
194
|
+
else
|
|
195
|
+
printWarning('Could not open browser — open the URL manually');
|
|
196
|
+
printInfo('Waiting for authorization...');
|
|
197
|
+
const result = await pollForToken(dc.deviceCode, dc.interval, oidcConfig);
|
|
198
|
+
const accessToken = result.accessToken;
|
|
199
|
+
const expiresAt = Date.now() + result.expiresIn * 1000;
|
|
200
|
+
printInfo('Fetching workspaces...');
|
|
201
|
+
try {
|
|
202
|
+
const workspaces = await fetchWorkspaces(config.globalEndpoint, accessToken);
|
|
203
|
+
if (workspaces.length === 0) {
|
|
204
|
+
writeAuth({
|
|
205
|
+
accessToken,
|
|
206
|
+
refreshToken: result.refreshToken,
|
|
207
|
+
expiresAt,
|
|
208
|
+
authMethod: 'device_code',
|
|
209
|
+
});
|
|
210
|
+
resetClients();
|
|
211
|
+
printSuccess('Authentication successful (no workspaces found)');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let selected = workspaces[0];
|
|
215
|
+
if (workspaces.length > 1) {
|
|
216
|
+
console.log('\nAvailable workspaces:');
|
|
217
|
+
workspaces.forEach((ws, i) => console.log(` ${i + 1}. ${ws.name} (${ws.slug})`));
|
|
218
|
+
const readline = await import('readline');
|
|
219
|
+
// Re-prompt up to 3 times on invalid input rather than silently
|
|
220
|
+
// defaulting to workspaces[0] — users who mistype expect to see
|
|
221
|
+
// an error, not end up in the wrong workspace.
|
|
222
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
223
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
224
|
+
const answer = await new Promise((resolve) => {
|
|
225
|
+
rl.question(`\nSelect workspace [1-${workspaces.length}]: `, (a) => {
|
|
226
|
+
rl.close();
|
|
227
|
+
resolve(a.trim());
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
const idx = parseInt(answer, 10) - 1;
|
|
231
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < workspaces.length) {
|
|
232
|
+
selected = workspaces[idx];
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (attempt < 2) {
|
|
236
|
+
printError(`Invalid selection. Enter a number between 1 and ${workspaces.length}.`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
printError('Too many invalid attempts — aborting login.');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
printInfo(`Using workspace: ${selected.name} (${selected.id})`);
|
|
245
|
+
const wsResult = await issueWorkspaceToken(config.workspaceEndpoint, accessToken, selected.id, selected.organizationId);
|
|
246
|
+
writeAuth({
|
|
247
|
+
accessToken,
|
|
248
|
+
refreshToken: result.refreshToken,
|
|
249
|
+
expiresAt,
|
|
250
|
+
workspaceToken: wsResult.token,
|
|
251
|
+
workspaceTokenExpiresAt: new Date(wsResult.expiresAt).getTime(),
|
|
252
|
+
workspaceId: selected.id,
|
|
253
|
+
organizationId: selected.organizationId,
|
|
254
|
+
authMethod: 'device_code',
|
|
255
|
+
});
|
|
256
|
+
resetClients();
|
|
257
|
+
printSuccess('Authentication successful');
|
|
258
|
+
}
|
|
259
|
+
catch (wsError) {
|
|
260
|
+
writeAuth({
|
|
261
|
+
accessToken,
|
|
262
|
+
refreshToken: result.refreshToken,
|
|
263
|
+
expiresAt,
|
|
264
|
+
authMethod: 'device_code',
|
|
265
|
+
});
|
|
266
|
+
resetClients();
|
|
267
|
+
printSuccess('Authentication successful');
|
|
268
|
+
printWarning(`Workspace token not obtained: ${wsError instanceof Error ? wsError.message : String(wsError)}`);
|
|
269
|
+
printInfo('Run `bigconsole auth login` again once a workspace is available.');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function loginWithClientCredentials(workspaceId) {
|
|
273
|
+
const config = readConfig();
|
|
274
|
+
const readline = await import('readline');
|
|
275
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
276
|
+
const clientId = await new Promise((resolve) => {
|
|
277
|
+
rl.question('Client ID: ', (a) => resolve(a.trim()));
|
|
278
|
+
});
|
|
279
|
+
// Mask the client-secret input so the value doesn't echo into the terminal,
|
|
280
|
+
// get captured by screen recordings, or land in shell scroll-back. We
|
|
281
|
+
// temporarily intercept stdout writes that readline emits for each keystroke
|
|
282
|
+
// and suppress them — readline still sees the real chars because
|
|
283
|
+
// input.on('data') runs before output, so the resulting string is unchanged.
|
|
284
|
+
const out = rl;
|
|
285
|
+
out._writeToOutput = function maskedWrite(stringToWrite) {
|
|
286
|
+
if (stringToWrite === '\r\n' || stringToWrite === '\n') {
|
|
287
|
+
out.output.write(stringToWrite);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// readline sends the raw user char here — replace with a mask.
|
|
291
|
+
out.output.write('*');
|
|
292
|
+
};
|
|
293
|
+
const clientSecret = await new Promise((resolve) => {
|
|
294
|
+
rl.question('Client Secret: ', (a) => {
|
|
295
|
+
rl.close();
|
|
296
|
+
resolve(a.trim());
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
// Newline after the masked line — otherwise the prompt runs into the next
|
|
300
|
+
// line of output.
|
|
301
|
+
process.stdout.write('\n');
|
|
302
|
+
if (!clientId || !clientSecret) {
|
|
303
|
+
printError('Client ID and secret cannot be empty');
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
printInfo('Authenticating app...');
|
|
307
|
+
const result = await clientCredentialsFlow(config.globalEndpoint, clientId, clientSecret);
|
|
308
|
+
const accessToken = result.accessToken;
|
|
309
|
+
const expiresAt = Date.now() + result.expiresIn * 1000;
|
|
310
|
+
let wsToken;
|
|
311
|
+
let wsTokenExpiresAt;
|
|
312
|
+
let wsId = workspaceId;
|
|
313
|
+
let orgId;
|
|
314
|
+
if (wsId) {
|
|
315
|
+
// When the user passed --workspace-id they explicitly asked for a
|
|
316
|
+
// workspace-scoped session — fail the login (not just warn) if we
|
|
317
|
+
// can't produce a workspace token, otherwise the CLI would persist
|
|
318
|
+
// a platform-only session and every subsequent workspace call would
|
|
319
|
+
// 401 with a confusing "not authenticated for this workspace" error.
|
|
320
|
+
try {
|
|
321
|
+
const workspaces = await fetchWorkspaces(config.globalEndpoint, accessToken);
|
|
322
|
+
const ws = workspaces.find((w) => w.id === wsId);
|
|
323
|
+
if (!ws) {
|
|
324
|
+
printError(`Workspace ${wsId} not found or not accessible to this app.`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
orgId = ws.organizationId;
|
|
328
|
+
const wsResult = await issueWorkspaceToken(config.workspaceEndpoint, accessToken, wsId, orgId);
|
|
329
|
+
wsToken = wsResult.token;
|
|
330
|
+
wsTokenExpiresAt = new Date(wsResult.expiresAt).getTime();
|
|
331
|
+
printInfo(`Workspace token issued for: ${ws.name}`);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
printError(`Workspace token issuance failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
writeAuth({
|
|
339
|
+
accessToken,
|
|
340
|
+
refreshToken: '',
|
|
341
|
+
expiresAt,
|
|
342
|
+
workspaceToken: wsToken,
|
|
343
|
+
workspaceTokenExpiresAt: wsTokenExpiresAt,
|
|
344
|
+
workspaceId: wsId,
|
|
345
|
+
organizationId: orgId,
|
|
346
|
+
authMethod: 'client_credentials',
|
|
347
|
+
clientId,
|
|
348
|
+
clientSecret,
|
|
349
|
+
});
|
|
350
|
+
resetClients();
|
|
351
|
+
printSuccess('App authentication successful');
|
|
352
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared command utilities.
|
|
3
|
+
* Provides auth wrappers, format helpers, and error handling for all commands.
|
|
4
|
+
*/
|
|
5
|
+
import type { RequestDocument, Variables } from 'graphql-request';
|
|
6
|
+
/**
|
|
7
|
+
* Load a JSON-valued command input from either --file or --json, mutually
|
|
8
|
+
* exclusive. Returns undefined if neither is provided. Exits with a clear
|
|
9
|
+
* error if the JSON is malformed or both flags are set.
|
|
10
|
+
*
|
|
11
|
+
* Used for complex inputs (widget.config, parser.expression, pipeline.stages,
|
|
12
|
+
* dashboard.definition) where a single flag value would be unwieldy.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadJsonInput<T = unknown>(file?: string, inline?: string): T | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Read a raw string payload — used for parser expressions and other text
|
|
17
|
+
* bodies where `--body-file foo.jq` reads the file and `--body 'raw'` takes
|
|
18
|
+
* the literal string. Mutually exclusive.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadTextInput(file?: string, inline?: string): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Ensure the user is authenticated before running a command.
|
|
23
|
+
* Exits with code 1 if not authenticated.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requireAuth(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Execute a workspace GraphQL request with error handling.
|
|
28
|
+
*/
|
|
29
|
+
export declare function execWorkspace<T>(document: RequestDocument, variables?: Variables): Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Execute a global GraphQL request with error handling.
|
|
32
|
+
*/
|
|
33
|
+
export declare function execGlobal<T>(document: RequestDocument, variables?: Variables): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Handle command errors consistently, printing user-friendly messages.
|
|
36
|
+
*/
|
|
37
|
+
export declare function handleCommandError(error: unknown): never;
|
|
38
|
+
/**
|
|
39
|
+
* Get format and output data for a command.
|
|
40
|
+
* Standard pattern used by all list/get commands.
|
|
41
|
+
*/
|
|
42
|
+
export declare function formatAndOutput(options: {
|
|
43
|
+
format?: string;
|
|
44
|
+
}, data: unknown, tableConfig?: {
|
|
45
|
+
headers: string[];
|
|
46
|
+
rows: string[][];
|
|
47
|
+
}): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared command utilities.
|
|
3
|
+
* Provides auth wrappers, format helpers, and error handling for all commands.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { readAuth } from '../auth/store.js';
|
|
7
|
+
import { workspaceRequest, globalRequest } from '../graphql/client.js';
|
|
8
|
+
import { printError, getFormat, outputData } from '../utils/output.js';
|
|
9
|
+
/**
|
|
10
|
+
* Load a JSON-valued command input from either --file or --json, mutually
|
|
11
|
+
* exclusive. Returns undefined if neither is provided. Exits with a clear
|
|
12
|
+
* error if the JSON is malformed or both flags are set.
|
|
13
|
+
*
|
|
14
|
+
* Used for complex inputs (widget.config, parser.expression, pipeline.stages,
|
|
15
|
+
* dashboard.definition) where a single flag value would be unwieldy.
|
|
16
|
+
*/
|
|
17
|
+
export function loadJsonInput(file, inline) {
|
|
18
|
+
if (!file && !inline)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (file && inline) {
|
|
21
|
+
printError('Provide --file OR --json, not both');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const raw = file ? readFileSync(file, 'utf8') : inline;
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
printError(`Failed to parse JSON input: ${msg}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read a raw string payload — used for parser expressions and other text
|
|
36
|
+
* bodies where `--body-file foo.jq` reads the file and `--body 'raw'` takes
|
|
37
|
+
* the literal string. Mutually exclusive.
|
|
38
|
+
*/
|
|
39
|
+
export function loadTextInput(file, inline) {
|
|
40
|
+
if (!file && !inline)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (file && inline) {
|
|
43
|
+
printError('Provide --file OR inline value, not both');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return file ? readFileSync(file, 'utf8') : inline;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
printError(`Failed to read file: ${msg}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Ensure the user is authenticated before running a command.
|
|
57
|
+
* Exits with code 1 if not authenticated.
|
|
58
|
+
*/
|
|
59
|
+
export function requireAuth() {
|
|
60
|
+
const auth = readAuth();
|
|
61
|
+
if (!auth?.accessToken) {
|
|
62
|
+
printError('Authentication required. Run: bigconsole auth login');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
// We intentionally DO NOT hard-fail on expired access tokens here —
|
|
66
|
+
// `workspaceRequest`/`globalRequest` both call `ensureFreshTokens()`,
|
|
67
|
+
// which transparently refreshes near-expiry tokens. Bailing at this
|
|
68
|
+
// point would short-circuit that refresh and force users to run
|
|
69
|
+
// `bigconsole auth refresh` manually for every command after expiry.
|
|
70
|
+
// We only fail when there are no credentials at all.
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Execute a workspace GraphQL request with error handling.
|
|
74
|
+
*/
|
|
75
|
+
export async function execWorkspace(document, variables) {
|
|
76
|
+
requireAuth();
|
|
77
|
+
return workspaceRequest(document, variables);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Execute a global GraphQL request with error handling.
|
|
81
|
+
*/
|
|
82
|
+
export async function execGlobal(document, variables) {
|
|
83
|
+
return globalRequest(document, variables);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Handle command errors consistently, printing user-friendly messages.
|
|
87
|
+
*/
|
|
88
|
+
export function handleCommandError(error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
printError(message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get format and output data for a command.
|
|
95
|
+
* Standard pattern used by all list/get commands.
|
|
96
|
+
*/
|
|
97
|
+
export function formatAndOutput(options, data, tableConfig) {
|
|
98
|
+
const format = getFormat(options);
|
|
99
|
+
outputData(format, data, tableConfig);
|
|
100
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli-config commands — manage CLI-level endpoint + auth configuration.
|
|
3
|
+
* Separate from `config` (which will later manage platform-level BigConsole config).
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
export declare function registerCLIConfigCommands(program: Command): void;
|