@burdenoff/bigconsole-cli 2026.802.1 → 2026.1110.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.
@@ -1,352 +0,0 @@
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
- }
@@ -1,100 +0,0 @@
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
- }
@@ -1,79 +0,0 @@
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 { readConfig, writeConfig, getConfigPath, DEFAULT_CONFIG, KNOWN_CONFIG_KEYS, } from '../config/index.js';
6
- import { printSuccess, printError, formatDate } from '../utils/output.js';
7
- import { formatAndOutput, handleCommandError } from './base.js';
8
- export function registerCLIConfigCommands(program) {
9
- const cli = program.command('cli-config').description('CLI configuration (endpoints, client ID)');
10
- cli
11
- .command('show')
12
- .description('Show current CLI configuration')
13
- .option('-f, --format <format>', 'Output format (json, table, yaml)')
14
- .action((options) => {
15
- const config = readConfig();
16
- formatAndOutput(options, config, {
17
- headers: ['Key', 'Value'],
18
- rows: Object.entries(config).map(([k, v]) => [k, v == null ? '-' : String(v)]),
19
- });
20
- });
21
- cli
22
- .command('path')
23
- .description('Show CLI config file path')
24
- .action(() => {
25
- console.log(getConfigPath());
26
- });
27
- cli
28
- .command('get <key>')
29
- .description('Get a single config value')
30
- .action((key) => {
31
- try {
32
- if (!KNOWN_CONFIG_KEYS.includes(key)) {
33
- printError(`Unknown config key: ${key}. Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}`);
34
- process.exit(1);
35
- }
36
- const config = readConfig();
37
- const value = config[key];
38
- if (value === undefined || value === null) {
39
- console.log('-');
40
- }
41
- else {
42
- console.log(String(value));
43
- }
44
- }
45
- catch (error) {
46
- handleCommandError(error);
47
- }
48
- });
49
- cli
50
- .command('set <key> <value>')
51
- .description('Set a config value')
52
- .action((key, value) => {
53
- try {
54
- if (!KNOWN_CONFIG_KEYS.includes(key)) {
55
- printError(`Unknown config key: ${key}. Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}`);
56
- process.exit(1);
57
- }
58
- const config = readConfig();
59
- config[key] = value;
60
- writeConfig(config);
61
- printSuccess(`Set ${key} = ${value} (last updated ${formatDate(new Date().toISOString())})`);
62
- }
63
- catch (error) {
64
- handleCommandError(error);
65
- }
66
- });
67
- cli
68
- .command('reset')
69
- .description('Reset to default CLI configuration')
70
- .action(() => {
71
- try {
72
- writeConfig({ ...DEFAULT_CONFIG });
73
- printSuccess('CLI configuration reset to defaults');
74
- }
75
- catch (error) {
76
- handleCommandError(error);
77
- }
78
- });
79
- }