@hubspot/cli 8.8.0 → 8.9.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/account/auth.d.ts +3 -1
- package/commands/account/auth.js +17 -1
- package/commands/app/logDetails.d.ts +9 -0
- package/commands/app/logDetails.js +86 -0
- package/commands/app/logs.d.ts +13 -0
- package/commands/app/logs.js +122 -0
- package/commands/app.js +8 -1
- package/commands/auth.js +1 -1
- package/commands/init.js +1 -1
- package/commands/project/lint.js +8 -0
- package/lang/en.d.ts +118 -1
- package/lang/en.js +119 -3
- package/lib/CLIWebSocketServer.d.ts +5 -3
- package/lib/CLIWebSocketServer.js +31 -4
- package/lib/accountAuth.d.ts +3 -1
- package/lib/accountAuth.js +43 -17
- package/lib/api/usageTracking.d.ts +1 -0
- package/lib/api/usageTracking.js +0 -17
- package/lib/app/logs.d.ts +38 -0
- package/lib/app/logs.js +225 -0
- package/lib/app/urls.d.ts +2 -0
- package/lib/app/urls.js +7 -0
- package/lib/auth/awaitPersonalAccessKeyOverWebsocket.d.ts +4 -0
- package/lib/auth/awaitPersonalAccessKeyOverWebsocket.js +145 -0
- package/lib/buildAccount.js +1 -1
- package/lib/constants.d.ts +8 -0
- package/lib/constants.js +8 -0
- package/lib/middleware/commandTargetingUtils.js +1 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.js +1 -1
- package/lib/projects/workspaces.d.ts +14 -6
- package/lib/projects/workspaces.js +75 -29
- package/lib/prompts/personalAccessKeyPrompt.d.ts +2 -5
- package/lib/prompts/personalAccessKeyPrompt.js +7 -5
- package/lib/prompts/selectAppPrompt.js +1 -0
- package/lib/prompts/setAsDefaultAccountPrompt.js +2 -1
- package/lib/serverlessLogs.js +2 -2
- package/lib/ui/appLogs.d.ts +32 -0
- package/lib/ui/appLogs.js +175 -0
- package/lib/usageTracking.js +28 -4
- package/mcp-server/utils/command.js +3 -1
- package/mcp-server/utils/config.js +1 -0
- package/package.json +4 -4
- package/ui/components/ActionSection.d.ts +1 -1
- package/ui/components/InputField.d.ts +1 -1
- package/ui/components/SelectInput.d.ts +1 -1
- package/ui/components/StatusIcon.d.ts +1 -1
- package/ui/components/Table.d.ts +5 -5
- package/ui/components/getStarted/GetStartedFlow.d.ts +1 -1
- package/ui/components/getStarted/screens/InstallationScreen.d.ts +1 -1
- package/ui/components/getStarted/screens/ProjectSetupScreen.d.ts +1 -1
- package/ui/components/getStarted/screens/UploadScreen.d.ts +1 -1
package/lang/en.js
CHANGED
|
@@ -2,12 +2,11 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { mapToUserFriendlyName } from '@hubspot/project-parsing-lib/transform';
|
|
3
3
|
import { PLATFORM_VERSIONS } from '@hubspot/project-parsing-lib/constants';
|
|
4
4
|
import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
|
|
5
|
-
import { LOCAL_DEV_DEFAULT_PORT } from '../lib/constants.js';
|
|
5
|
+
import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, LEGACY_PUBLIC_APP_FILE, LOCAL_DEV_DEFAULT_PORT, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
|
|
6
6
|
import { ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, GLOBAL_CONFIG_PATH, } from '@hubspot/local-dev-lib/constants/config';
|
|
7
7
|
import { indent, UI_COLORS, uiAccountDescription, uiAuthCommandReference, uiBetaTag, uiCommandReference, uiLink, } from '../lib/ui/index.js';
|
|
8
8
|
import { getLocalDevUiUrl, getProjectDetailUrl, getProjectSettingsUrl, } from '../lib/projects/urls.js';
|
|
9
9
|
import { getProductUpdatesUrl } from '../lib/links.js';
|
|
10
|
-
import { APP_AUTH_TYPES, APP_DISTRIBUTION_TYPES, LEGACY_PUBLIC_APP_FILE, PROJECT_CONFIG_FILE, PROJECT_WITH_APP, } from '../lib/constants.js';
|
|
11
10
|
export const commands = {
|
|
12
11
|
generalErrors: {
|
|
13
12
|
srcIsProject: (src, command) => `"${src}" is in a project folder. Did you mean "hs project ${command}"?`,
|
|
@@ -124,6 +123,9 @@ export const commands = {
|
|
|
124
123
|
options: {
|
|
125
124
|
account: 'HubSpot account to authenticate',
|
|
126
125
|
personalAccessKey: 'Enter existing personal access key',
|
|
126
|
+
default: 'Set the authenticated account as the default account',
|
|
127
|
+
name: 'Set a name for the account in the CLI config',
|
|
128
|
+
useDefaultName: 'Use the account name derived from the HubSpot portal name',
|
|
127
129
|
},
|
|
128
130
|
errors: {
|
|
129
131
|
invalidAccountIdProvided: `--account must be a number.`,
|
|
@@ -2040,6 +2042,7 @@ export const commands = {
|
|
|
2040
2042
|
return `The dependencies required for linting are missing or outdated.\n\nDependencies:\n${uniquePackages.map(p => ` - ${p}`).join('\n')}\n\n${directories.length === 1 ? 'Directory' : 'Directories'}:\n${directories.map(d => ` - ${d}`).join('\n')}\n\nWould you like to install the required dependencies?`;
|
|
2041
2043
|
},
|
|
2042
2044
|
skippingDirectoriesWarning: (directories) => `Skipping linting for the following ${directories.length === 1 ? 'directory' : 'directories'}:\n${directories.map(d => ` - ${d}`).join('\n')}`,
|
|
2045
|
+
skippedDirectoriesError: (directoryCount) => `Linting could not run for ${directoryCount === 1 ? 'the directory' : 'the directories'} above because the required dependencies are missing. Run ${uiCommandReference('hs project lint --install-missing-deps')} to install them.`,
|
|
2043
2046
|
deprecatedEslintConfigWarning: (details) => {
|
|
2044
2047
|
const dirCount = details.length;
|
|
2045
2048
|
const header = `Deprecated ESLint configuration file${dirCount === 1 ? '' : 's'} detected in the following ${dirCount === 1 ? 'directory' : 'directories'}:`;
|
|
@@ -2402,6 +2405,111 @@ export const commands = {
|
|
|
2402
2405
|
},
|
|
2403
2406
|
},
|
|
2404
2407
|
},
|
|
2408
|
+
logs: {
|
|
2409
|
+
describe: 'View recent application logs.',
|
|
2410
|
+
verboseDescribe: `View recent application logs for a specific log type.\n\nFilter by time range with ${uiCommandReference('--since')} (e.g. 1h, 30m, 2d, or an ISO timestamp). Use ${uiCommandReference('--tail')} to follow logs in real-time, ${uiCommandReference('--compact')} for one-line-per-log output, or ${uiCommandReference('--json')} for machine-readable output.\n\nRun ${uiCommandReference('hs app log-details <logId>')} to inspect a specific log entry.`,
|
|
2411
|
+
options: {
|
|
2412
|
+
appId: 'App ID',
|
|
2413
|
+
type: 'Log type',
|
|
2414
|
+
json: 'Output logs as JSON',
|
|
2415
|
+
limit: 'Maximum number of logs to return',
|
|
2416
|
+
since: 'Filter logs by time ago (1h, 30m, 2d, or ISO timestamp)',
|
|
2417
|
+
tail: 'Follow logs in real-time',
|
|
2418
|
+
errorsOnly: 'Only show error logs',
|
|
2419
|
+
compact: 'Display logs in compact format (one line per log)',
|
|
2420
|
+
},
|
|
2421
|
+
errors: {
|
|
2422
|
+
noLogs: 'No logs found. Try a wider time range (--since) or remove --errors-only to see all logs.',
|
|
2423
|
+
noApps: `No apps found. Create an app with ${uiCommandReference('hs project create')}.`,
|
|
2424
|
+
},
|
|
2425
|
+
prompts: {
|
|
2426
|
+
selectType: '[--type] Select the type of logs to view:',
|
|
2427
|
+
},
|
|
2428
|
+
examples: {
|
|
2429
|
+
basic: 'Fetch webhook logs for app 123456',
|
|
2430
|
+
since: 'Fetch logs from the last hour',
|
|
2431
|
+
tail: 'Follow logs in real-time',
|
|
2432
|
+
json: 'Output logs as JSON',
|
|
2433
|
+
compact: 'Display logs in compact format',
|
|
2434
|
+
},
|
|
2435
|
+
outputMessages: {
|
|
2436
|
+
viewInHubSpot: (url) => uiLink('View in HubSpot', url),
|
|
2437
|
+
tableHeaders: {
|
|
2438
|
+
appId: 'App ID',
|
|
2439
|
+
type: 'Type',
|
|
2440
|
+
logsFound: 'Logs Found',
|
|
2441
|
+
},
|
|
2442
|
+
logDetails: {
|
|
2443
|
+
id: 'ID',
|
|
2444
|
+
duration: 'Duration',
|
|
2445
|
+
portal: 'Portal',
|
|
2446
|
+
trace: 'Trace',
|
|
2447
|
+
error: 'Error',
|
|
2448
|
+
viewDetails: (logId, appId, systemType) => `To view more details, run: ${uiCommandReference(`hs app log-details ${logId} --app=${appId} --type=${systemType}`)}`,
|
|
2449
|
+
viewInUI: (url) => uiLink('View details in UI', url),
|
|
2450
|
+
},
|
|
2451
|
+
tailMessages: {
|
|
2452
|
+
following: (appId) => `Following logs for app ${appId}`,
|
|
2453
|
+
stop: `> Press ${chalk.bold('q')} to stop following`,
|
|
2454
|
+
},
|
|
2455
|
+
},
|
|
2456
|
+
},
|
|
2457
|
+
logDetails: {
|
|
2458
|
+
describe: 'View details for a specific log entry.',
|
|
2459
|
+
verboseDescribe: `View full details for a specific app log entry, including request/response bodies, context information, and error details.\n\nUse ${uiCommandReference('--json')} for machine-readable output.`,
|
|
2460
|
+
positionals: {
|
|
2461
|
+
logId: 'The log ID to fetch details for',
|
|
2462
|
+
},
|
|
2463
|
+
options: {
|
|
2464
|
+
appId: 'App ID',
|
|
2465
|
+
type: 'Log type',
|
|
2466
|
+
json: 'Output details as JSON',
|
|
2467
|
+
},
|
|
2468
|
+
errors: {
|
|
2469
|
+
noApps: `No apps found. Create an app with ${uiCommandReference('hs project create')}.`,
|
|
2470
|
+
},
|
|
2471
|
+
prompts: {
|
|
2472
|
+
selectType: '[--type] Select the log type:',
|
|
2473
|
+
},
|
|
2474
|
+
examples: {
|
|
2475
|
+
basic: 'Fetch details for log abc-123',
|
|
2476
|
+
json: 'Output log details as JSON',
|
|
2477
|
+
},
|
|
2478
|
+
outputMessages: {
|
|
2479
|
+
viewInHubSpot: (url) => uiLink('View in HubSpot', url),
|
|
2480
|
+
logDetailsHeader: 'Log Details',
|
|
2481
|
+
basicInfo: {
|
|
2482
|
+
id: 'Log ID',
|
|
2483
|
+
timestamp: 'Timestamp',
|
|
2484
|
+
status: 'Status',
|
|
2485
|
+
duration: 'Duration',
|
|
2486
|
+
systemType: 'Type',
|
|
2487
|
+
},
|
|
2488
|
+
contextInfo: {
|
|
2489
|
+
header: 'Context',
|
|
2490
|
+
portalId: 'Portal ID',
|
|
2491
|
+
traceId: 'Trace ID',
|
|
2492
|
+
function: 'Function',
|
|
2493
|
+
location: 'Location',
|
|
2494
|
+
card: 'Card',
|
|
2495
|
+
userId: 'User ID',
|
|
2496
|
+
},
|
|
2497
|
+
requestResponse: {
|
|
2498
|
+
header: 'Request/Response',
|
|
2499
|
+
requestBody: 'Request Body',
|
|
2500
|
+
responseBody: 'Response Body',
|
|
2501
|
+
},
|
|
2502
|
+
errorInfo: {
|
|
2503
|
+
header: 'Error Details',
|
|
2504
|
+
errorType: 'Type',
|
|
2505
|
+
errorMessage: 'Message',
|
|
2506
|
+
stackTrace: 'Stack Trace',
|
|
2507
|
+
},
|
|
2508
|
+
additionalInfo: {
|
|
2509
|
+
header: 'Additional Information',
|
|
2510
|
+
},
|
|
2511
|
+
},
|
|
2512
|
+
},
|
|
2405
2513
|
},
|
|
2406
2514
|
},
|
|
2407
2515
|
secret: {
|
|
@@ -3273,6 +3381,13 @@ export const lib = {
|
|
|
3273
3381
|
appDataNotFound: 'An error occurred while fetching data for your app.',
|
|
3274
3382
|
oauthAppRedirectUrlError: (redirectUrl) => `${chalk.bold('No reponse from your OAuth service:')} ${redirectUrl}\nYour app needs a valid OAuth2 service to be installed for local dev. ${uiLink('Learn more', 'https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/oauth/working-with-oauth')}`,
|
|
3275
3383
|
},
|
|
3384
|
+
accountAuthWebsocket: {
|
|
3385
|
+
logs: {
|
|
3386
|
+
openingWebBrowser: (url) => `Opening ${uiLink('HubSpot', url)} in your web browser\n`,
|
|
3387
|
+
spinner: 'Waiting for HubSpot to send your personal access key... (press any key to enter it manually)',
|
|
3388
|
+
received: 'Personal access key received',
|
|
3389
|
+
},
|
|
3390
|
+
},
|
|
3276
3391
|
CLIWebsocketServer: {
|
|
3277
3392
|
errors: {
|
|
3278
3393
|
portManagerNotRunning: (prefix) => `${prefix ? `${prefix} ` : ''}PortManagerServing must be running before starting WebsocketServer.`,
|
|
@@ -3280,6 +3395,7 @@ export const lib = {
|
|
|
3280
3395
|
missingTypeField: (data) => `Unsupported message received. Missing type field: ${data}`,
|
|
3281
3396
|
invalidJSON: (data) => `Unsupported message received. Invalid JSON: ${data}`,
|
|
3282
3397
|
unknownMessageType: (type) => `Unsupported message received. Unknown message type: ${type}`,
|
|
3398
|
+
failedToBindEphemeralPort: (prefix) => `${prefix ? `${prefix} ` : ''}Failed to determine the assigned port for the WebsocketServer.`,
|
|
3283
3399
|
},
|
|
3284
3400
|
logs: {
|
|
3285
3401
|
startup: (port) => `WebsocketServer running on port ${port}`,
|
|
@@ -3748,7 +3864,7 @@ export const lib = {
|
|
|
3748
3864
|
functionName: (projectName) => `[--function] Select function in ${chalk.bold(projectName)} project`,
|
|
3749
3865
|
},
|
|
3750
3866
|
setAsDefaultAccountPrompt: {
|
|
3751
|
-
setAsDefaultAccountMessage:
|
|
3867
|
+
setAsDefaultAccountMessage: (accountName) => `Set ${accountName} as your default account? [--default]`,
|
|
3752
3868
|
setAsDefaultAccount: (accountName) => `Account "${accountName}" set as the default account`,
|
|
3753
3869
|
keepingCurrentDefault: (accountName) => `Account "${accountName}" will continue to be the default account`,
|
|
3754
3870
|
},
|
|
@@ -5,11 +5,11 @@ export type CLIWebSocketMessage = {
|
|
|
5
5
|
};
|
|
6
6
|
declare class CLIWebSocketServer {
|
|
7
7
|
private server?;
|
|
8
|
-
private instanceId
|
|
8
|
+
private instanceId?;
|
|
9
9
|
private logPrefix?;
|
|
10
10
|
private debug?;
|
|
11
11
|
constructor({ instanceId, logPrefix, debug, }: {
|
|
12
|
-
instanceId
|
|
12
|
+
instanceId?: string;
|
|
13
13
|
logPrefix?: string;
|
|
14
14
|
debug?: boolean;
|
|
15
15
|
});
|
|
@@ -17,12 +17,14 @@ declare class CLIWebSocketServer {
|
|
|
17
17
|
private logError;
|
|
18
18
|
sendMessage(websocket: WebSocket, message: CLIWebSocketMessage): void;
|
|
19
19
|
private sendCliMetadata;
|
|
20
|
+
private bindPortManagerPort;
|
|
21
|
+
private bindEphemeralPort;
|
|
20
22
|
start({ onConnection, onMessage, onClose, metadata, }: {
|
|
21
23
|
onConnection?: (websocket: WebSocket) => void;
|
|
22
24
|
onMessage?: (websocket: WebSocket, message: CLIWebSocketMessage) => boolean;
|
|
23
25
|
onClose?: () => void;
|
|
24
26
|
metadata?: Record<string, unknown>;
|
|
25
|
-
}): Promise<
|
|
27
|
+
}): Promise<number>;
|
|
26
28
|
shutdown(): void;
|
|
27
29
|
}
|
|
28
30
|
export default CLIWebSocketServer;
|
|
@@ -39,14 +39,40 @@ class CLIWebSocketServer {
|
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
async
|
|
42
|
+
async bindPortManagerPort(instanceId) {
|
|
43
43
|
const portManagerIsRunning = await isPortManagerServerRunning();
|
|
44
44
|
if (!portManagerIsRunning) {
|
|
45
45
|
throw new Error(lib.CLIWebsocketServer.errors.portManagerNotRunning(this.logPrefix));
|
|
46
46
|
}
|
|
47
|
-
const portData = await requestPorts([{ instanceId
|
|
48
|
-
const port = portData[
|
|
49
|
-
|
|
47
|
+
const portData = await requestPorts([{ instanceId }]);
|
|
48
|
+
const port = portData[instanceId];
|
|
49
|
+
return { server: new WebSocketServer({ port }), port };
|
|
50
|
+
}
|
|
51
|
+
async bindEphemeralPort() {
|
|
52
|
+
const server = new WebSocketServer({ port: 0 });
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
const onListening = () => {
|
|
55
|
+
server.off('error', onError);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
const onError = (err) => {
|
|
59
|
+
server.off('listening', onListening);
|
|
60
|
+
reject(err);
|
|
61
|
+
};
|
|
62
|
+
server.once('listening', onListening);
|
|
63
|
+
server.once('error', onError);
|
|
64
|
+
});
|
|
65
|
+
const address = server.address();
|
|
66
|
+
if (!address || typeof address !== 'object') {
|
|
67
|
+
throw new Error(lib.CLIWebsocketServer.errors.failedToBindEphemeralPort(this.logPrefix));
|
|
68
|
+
}
|
|
69
|
+
return { server, port: address.port };
|
|
70
|
+
}
|
|
71
|
+
async start({ onConnection, onMessage, onClose, metadata, }) {
|
|
72
|
+
const { server, port } = this.instanceId
|
|
73
|
+
? await this.bindPortManagerPort(this.instanceId)
|
|
74
|
+
: await this.bindEphemeralPort();
|
|
75
|
+
this.server = server;
|
|
50
76
|
this.log(lib.CLIWebsocketServer.logs.startup(port));
|
|
51
77
|
this.server.on('connection', (ws, req) => {
|
|
52
78
|
const origin = req.headers.origin;
|
|
@@ -82,6 +108,7 @@ class CLIWebSocketServer {
|
|
|
82
108
|
onClose();
|
|
83
109
|
}
|
|
84
110
|
});
|
|
111
|
+
return port;
|
|
85
112
|
}
|
|
86
113
|
shutdown() {
|
|
87
114
|
this.server?.close();
|
package/lib/accountAuth.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ type AuthenticateNewAccountOptions = {
|
|
|
5
5
|
providedPersonalAccessKey?: string;
|
|
6
6
|
accountId?: number;
|
|
7
7
|
setAsDefaultAccount?: boolean;
|
|
8
|
+
accountName?: string;
|
|
9
|
+
useDefaultAccountName?: boolean;
|
|
8
10
|
};
|
|
9
|
-
export declare function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, }: AuthenticateNewAccountOptions): Promise<HubSpotConfigAccount | null>;
|
|
11
|
+
export declare function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, accountName: providedAccountName, useDefaultAccountName, }: AuthenticateNewAccountOptions): Promise<HubSpotConfigAccount | null>;
|
|
10
12
|
export {};
|
package/lib/accountAuth.js
CHANGED
|
@@ -1,29 +1,52 @@
|
|
|
1
|
-
import { updateConfigAccount, createEmptyConfigFile, getConfigFilePath, localConfigFileExists, globalConfigFileExists, setConfigAccountAsDefault, } from '@hubspot/local-dev-lib/config';
|
|
1
|
+
import { updateConfigAccount, createEmptyConfigFile, getConfigFilePath, localConfigFileExists, globalConfigFileExists, setConfigAccountAsDefault, getConfigDefaultAccountIfExists, } from '@hubspot/local-dev-lib/config';
|
|
2
2
|
import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
3
3
|
import { toKebabCase } from '@hubspot/local-dev-lib/text';
|
|
4
4
|
import { handleMerge, handleMigration } from './configMigrate.js';
|
|
5
5
|
import { debugError, logError } from './errorHandlers/index.js';
|
|
6
6
|
import { isPromptExitError } from './errors/PromptExitError.js';
|
|
7
|
-
import {
|
|
7
|
+
import { legacyPersonalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
|
|
8
8
|
import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
|
|
9
9
|
import { setAsDefaultAccountPrompt } from './prompts/setAsDefaultAccountPrompt.js';
|
|
10
|
+
import { awaitPersonalAccessKeyOverWebsocket } from './auth/awaitPersonalAccessKeyOverWebsocket.js';
|
|
10
11
|
import { commands } from '../lang/en.js';
|
|
11
12
|
import { uiLogger } from './ui/logger.js';
|
|
12
|
-
async function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
: await personalAccessKeyPrompt({
|
|
13
|
+
async function getPersonalAccessKey(env, accountId) {
|
|
14
|
+
if (process.env.BROWSER !== 'none') {
|
|
15
|
+
try {
|
|
16
|
+
return await awaitPersonalAccessKeyOverWebsocket({
|
|
17
17
|
env,
|
|
18
18
|
account: accountId,
|
|
19
19
|
});
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (isPromptExitError(e))
|
|
23
|
+
throw e;
|
|
24
|
+
debugError(e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const { personalAccessKey } = await legacyPersonalAccessKeyPrompt({
|
|
28
|
+
env,
|
|
29
|
+
account: accountId,
|
|
30
|
+
});
|
|
31
|
+
return personalAccessKey;
|
|
32
|
+
}
|
|
33
|
+
async function updateConfigWithNewAccount(env, configAlreadyExists, providedPersonalAccessKey, accountId, providedAccountName, useDefaultAccountName) {
|
|
34
|
+
try {
|
|
35
|
+
const personalAccessKey = providedPersonalAccessKey ?? (await getPersonalAccessKey(env, accountId));
|
|
20
36
|
const token = await getAccessToken(personalAccessKey, env);
|
|
21
37
|
const defaultAccountName = token.hubName
|
|
22
38
|
? toKebabCase(token.hubName)
|
|
23
39
|
: undefined;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
let accountName;
|
|
41
|
+
if (providedAccountName) {
|
|
42
|
+
accountName = providedAccountName;
|
|
43
|
+
}
|
|
44
|
+
else if (useDefaultAccountName && defaultAccountName) {
|
|
45
|
+
accountName = defaultAccountName;
|
|
46
|
+
}
|
|
47
|
+
else if (!configAlreadyExists) {
|
|
48
|
+
accountName = (await cliAccountNamePrompt(defaultAccountName)).name;
|
|
49
|
+
}
|
|
27
50
|
const updatedConfig = await updateConfigWithAccessToken(token, personalAccessKey, env, accountName, !configAlreadyExists);
|
|
28
51
|
if (!updatedConfig)
|
|
29
52
|
return null;
|
|
@@ -77,7 +100,7 @@ async function handleConfigMigration() {
|
|
|
77
100
|
return false;
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
|
-
export async function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, }) {
|
|
103
|
+
export async function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, accountName: providedAccountName, useDefaultAccountName, }) {
|
|
81
104
|
const configMigrationSuccess = await handleConfigMigration();
|
|
82
105
|
if (!configMigrationSuccess) {
|
|
83
106
|
return null;
|
|
@@ -86,7 +109,7 @@ export async function authenticateNewAccount({ env, providedPersonalAccessKey, a
|
|
|
86
109
|
if (!configAlreadyExists) {
|
|
87
110
|
createEmptyConfigFile(true);
|
|
88
111
|
}
|
|
89
|
-
const updatedConfig = await updateConfigWithNewAccount(env, configAlreadyExists, providedPersonalAccessKey, accountId);
|
|
112
|
+
const updatedConfig = await updateConfigWithNewAccount(env, configAlreadyExists, providedPersonalAccessKey, accountId, providedAccountName, useDefaultAccountName);
|
|
90
113
|
if (!updatedConfig) {
|
|
91
114
|
uiLogger.error(commands.account.subcommands.auth.errors.failedToUpdateConfig);
|
|
92
115
|
return null;
|
|
@@ -97,12 +120,15 @@ export async function authenticateNewAccount({ env, providedPersonalAccessKey, a
|
|
|
97
120
|
uiLogger.success(commands.account.subcommands.auth.success.configFileCreated(getConfigFilePath()));
|
|
98
121
|
uiLogger.success(commands.account.subcommands.auth.success.configFileUpdated(newAccountId));
|
|
99
122
|
}
|
|
100
|
-
else if (setAsDefaultAccount) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
else if (setAsDefaultAccount === true) {
|
|
124
|
+
const currentDefault = getConfigDefaultAccountIfExists();
|
|
125
|
+
if (currentDefault?.name !== name) {
|
|
126
|
+
setConfigAccountAsDefault(name);
|
|
127
|
+
uiLogger.log('');
|
|
128
|
+
uiLogger.success(commands.account.subcommands.auth.success.configFileUpdated(newAccountId));
|
|
129
|
+
}
|
|
104
130
|
}
|
|
105
|
-
else {
|
|
131
|
+
else if (setAsDefaultAccount !== false) {
|
|
106
132
|
await setAsDefaultAccountPrompt(name);
|
|
107
133
|
}
|
|
108
134
|
return updatedConfig;
|
package/lib/api/usageTracking.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
import { http } from '@hubspot/local-dev-lib/http';
|
|
2
1
|
import { http as unauthedHttp } from '@hubspot/local-dev-lib/http/unauthed';
|
|
3
|
-
import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
|
|
4
2
|
const USAGE_PATH = 'local/dev/tools/proxy/v1/usage';
|
|
5
|
-
const USAGE_AUTHENTICATED_PATH = `${USAGE_PATH}/authenticated`;
|
|
6
3
|
export async function sendUsageEvent(request) {
|
|
7
|
-
const { accountId } = request;
|
|
8
|
-
if (accountId) {
|
|
9
|
-
try {
|
|
10
|
-
const account = getConfigAccountById(accountId);
|
|
11
|
-
if (account?.authType === 'personalaccesskey') {
|
|
12
|
-
await http.post(accountId, {
|
|
13
|
-
url: USAGE_AUTHENTICATED_PATH,
|
|
14
|
-
data: request,
|
|
15
|
-
});
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
catch (_e) { }
|
|
20
|
-
}
|
|
21
4
|
try {
|
|
22
5
|
await unauthedHttp.post({
|
|
23
6
|
url: USAGE_PATH,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AppLogEntry as ApiAppLogEntry, SystemType } from '@hubspot/local-dev-lib/types/AppLogs';
|
|
2
|
+
export declare const SYSTEM_TYPE_DISPLAY_NAMES: {
|
|
3
|
+
[key: string]: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const SYSTEM_TYPE_CHOICES: string[];
|
|
6
|
+
export declare function parseSinceTime(sinceInput: string): {
|
|
7
|
+
startTime: number;
|
|
8
|
+
endTime: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function transformApiLogEntry(apiLog: ApiAppLogEntry): {
|
|
11
|
+
id: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
executionTimeMillis?: number;
|
|
14
|
+
portalId?: number;
|
|
15
|
+
traceId?: string;
|
|
16
|
+
status: 'SUCCESS' | 'ERROR';
|
|
17
|
+
errorType?: string;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
};
|
|
20
|
+
export type AppLogsOptions = {
|
|
21
|
+
since?: string;
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
json?: boolean;
|
|
24
|
+
limit?: number;
|
|
25
|
+
errorsOnly?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare function toTypeChoice(systemType: string): string;
|
|
28
|
+
export declare function toSystemType(choice: string): string;
|
|
29
|
+
export declare function getTypeChoices(): {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}[];
|
|
33
|
+
export declare const handleLogsRequest: (accountId: number, appId: number, systemType: string, options: AppLogsOptions) => Promise<void>;
|
|
34
|
+
export declare const tailAppLogs: (accountId: number, appId: number, systemType: string, options: AppLogsOptions) => Promise<void>;
|
|
35
|
+
export type LogDetailsOptions = {
|
|
36
|
+
json?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare const handleLogDetailsRequest: (accountId: number, appId: number, logId: string, systemType: SystemType, options: LogDetailsOptions) => Promise<void>;
|
package/lib/app/logs.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { getAppLogDetails, searchAppLogs, } from '@hubspot/local-dev-lib/api/appLogs';
|
|
3
|
+
import { outputAppLogDetails, outputAppLogs } from '../ui/appLogs.js';
|
|
4
|
+
import { uiLogger } from '../ui/logger.js';
|
|
5
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
6
|
+
import { handleExit, handleKeypress } from '../process.js';
|
|
7
|
+
import { commands } from '../../lang/en.js';
|
|
8
|
+
export const SYSTEM_TYPE_DISPLAY_NAMES = {
|
|
9
|
+
WEBHOOKS: 'Webhooks',
|
|
10
|
+
API_CALL: 'API Call',
|
|
11
|
+
SERVERLESS_EXECUTION: 'Serverless Function',
|
|
12
|
+
CRM_EXTENSIBILITY_CARD: 'CRM Card',
|
|
13
|
+
CRM_LEGACY_CARD: 'CRM Legacy Card',
|
|
14
|
+
EXTENSION_LOG: 'Extension Log',
|
|
15
|
+
EXTENSION_RENDER: 'Extension Render',
|
|
16
|
+
APP_SETTINGS: 'App Settings',
|
|
17
|
+
PROXY_EXECUTION: 'Proxy Execution',
|
|
18
|
+
SERVERLESS_GATEWAY_EXECUTION: 'Serverless Gateway Execution',
|
|
19
|
+
ACCEPTANCE_TEST: 'Acceptance Test',
|
|
20
|
+
OAUTH_AUTHORIZATION: 'OAuth Authorization',
|
|
21
|
+
};
|
|
22
|
+
export const SYSTEM_TYPE_CHOICES = Object.keys(SYSTEM_TYPE_DISPLAY_NAMES).map(toTypeChoice);
|
|
23
|
+
export function parseSinceTime(sinceInput) {
|
|
24
|
+
const now = moment();
|
|
25
|
+
const relativeTimePattern = /^(\d+)([mhd])$/;
|
|
26
|
+
const match = sinceInput.match(relativeTimePattern);
|
|
27
|
+
if (match) {
|
|
28
|
+
const value = parseInt(match[1], 10);
|
|
29
|
+
const unit = match[2];
|
|
30
|
+
let startTime;
|
|
31
|
+
switch (unit) {
|
|
32
|
+
case 'm':
|
|
33
|
+
startTime = now.clone().subtract(value, 'minutes');
|
|
34
|
+
break;
|
|
35
|
+
case 'h':
|
|
36
|
+
startTime = now.clone().subtract(value, 'hours');
|
|
37
|
+
break;
|
|
38
|
+
case 'd':
|
|
39
|
+
startTime = now.clone().subtract(value, 'days');
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`Invalid time unit: ${unit}`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
startTime: startTime.valueOf(),
|
|
46
|
+
endTime: now.valueOf(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const isoTime = moment(sinceInput);
|
|
50
|
+
if (isoTime.isValid()) {
|
|
51
|
+
return {
|
|
52
|
+
startTime: isoTime.valueOf(),
|
|
53
|
+
endTime: now.valueOf(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Invalid time format: ${sinceInput}`);
|
|
57
|
+
}
|
|
58
|
+
export function transformApiLogEntry(apiLog) {
|
|
59
|
+
return {
|
|
60
|
+
id: apiLog.id,
|
|
61
|
+
createdAt: apiLog.requestExecutionTimestamp,
|
|
62
|
+
executionTimeMillis: apiLog.duration,
|
|
63
|
+
portalId: apiLog.portalId,
|
|
64
|
+
traceId: apiLog.traceId,
|
|
65
|
+
status: apiLog.errorType ? 'ERROR' : 'SUCCESS',
|
|
66
|
+
errorType: apiLog.errorType,
|
|
67
|
+
errorMessage: apiLog.errorMessage,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const TAIL_DELAY = 5000;
|
|
71
|
+
export function toTypeChoice(systemType) {
|
|
72
|
+
return systemType.toLowerCase().replaceAll('_', '-');
|
|
73
|
+
}
|
|
74
|
+
export function toSystemType(choice) {
|
|
75
|
+
return choice.toUpperCase().replaceAll('-', '_');
|
|
76
|
+
}
|
|
77
|
+
export function getTypeChoices() {
|
|
78
|
+
return Object.entries(SYSTEM_TYPE_DISPLAY_NAMES).map(([key, name]) => ({
|
|
79
|
+
name,
|
|
80
|
+
value: key,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function buildTransformedResponse(data) {
|
|
84
|
+
return {
|
|
85
|
+
results: data.results.map(transformApiLogEntry),
|
|
86
|
+
hasMore: !!data.paging?.next,
|
|
87
|
+
offset: 0,
|
|
88
|
+
total: data.results.length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export const handleLogsRequest = async (accountId, appId, systemType, options) => {
|
|
92
|
+
const { since, compact, json, limit, errorsOnly } = options;
|
|
93
|
+
const resolvedLimit = limit ?? 50;
|
|
94
|
+
let timeRange;
|
|
95
|
+
if (since) {
|
|
96
|
+
timeRange = parseSinceTime(since);
|
|
97
|
+
}
|
|
98
|
+
const query = {
|
|
99
|
+
loggingSystemType: systemType,
|
|
100
|
+
limit: resolvedLimit,
|
|
101
|
+
offset: 0,
|
|
102
|
+
errorTypes: errorsOnly ? ['*'] : [],
|
|
103
|
+
resultsOrder: 'DESC',
|
|
104
|
+
...timeRange,
|
|
105
|
+
};
|
|
106
|
+
const requestBody = {
|
|
107
|
+
query,
|
|
108
|
+
limit: resolvedLimit,
|
|
109
|
+
};
|
|
110
|
+
const { data } = await searchAppLogs(accountId, appId, requestBody);
|
|
111
|
+
if (json) {
|
|
112
|
+
uiLogger.json(data);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
await outputAppLogs(buildTransformedResponse(data), {
|
|
116
|
+
compact: !!compact,
|
|
117
|
+
accountId,
|
|
118
|
+
appId,
|
|
119
|
+
systemType,
|
|
120
|
+
typeName: SYSTEM_TYPE_DISPLAY_NAMES[systemType],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
export const tailAppLogs = async (accountId, appId, systemType, options) => {
|
|
125
|
+
const { compact, since, errorsOnly } = options;
|
|
126
|
+
let currentAfter;
|
|
127
|
+
let currentStartTime;
|
|
128
|
+
if (since) {
|
|
129
|
+
currentStartTime = parseSinceTime(since).startTime;
|
|
130
|
+
}
|
|
131
|
+
return new Promise(resolve => {
|
|
132
|
+
function cleanup() {
|
|
133
|
+
SpinniesManager.remove('tailLogs');
|
|
134
|
+
SpinniesManager.remove('stopMessage');
|
|
135
|
+
}
|
|
136
|
+
let resolved = false;
|
|
137
|
+
// eslint-disable-next-line prefer-const -- assigned after onTerminate is defined due to circular reference
|
|
138
|
+
let removeExitListeners;
|
|
139
|
+
let timeoutHandle;
|
|
140
|
+
const onTerminate = async () => {
|
|
141
|
+
if (resolved)
|
|
142
|
+
return;
|
|
143
|
+
resolved = true;
|
|
144
|
+
removeExitListeners?.();
|
|
145
|
+
if (timeoutHandle) {
|
|
146
|
+
clearTimeout(timeoutHandle);
|
|
147
|
+
}
|
|
148
|
+
cleanup();
|
|
149
|
+
resolve();
|
|
150
|
+
};
|
|
151
|
+
removeExitListeners = handleExit(onTerminate);
|
|
152
|
+
handleKeypress(key => {
|
|
153
|
+
if ((key.ctrl && key.name === 'c') || key.name === 'q') {
|
|
154
|
+
onTerminate();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
async function tail() {
|
|
158
|
+
try {
|
|
159
|
+
const query = {
|
|
160
|
+
loggingSystemType: systemType,
|
|
161
|
+
limit: 50,
|
|
162
|
+
offset: 0,
|
|
163
|
+
errorTypes: errorsOnly ? ['*'] : [],
|
|
164
|
+
resultsOrder: 'ASC',
|
|
165
|
+
...(currentStartTime !== undefined && {
|
|
166
|
+
startTime: currentStartTime,
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
const requestBody = {
|
|
170
|
+
query,
|
|
171
|
+
limit: 50,
|
|
172
|
+
...(currentAfter && { after: currentAfter }),
|
|
173
|
+
};
|
|
174
|
+
const { data } = await searchAppLogs(accountId, appId, requestBody);
|
|
175
|
+
if (data.results && data.results.length > 0) {
|
|
176
|
+
await outputAppLogs(buildTransformedResponse(data), {
|
|
177
|
+
compact: !!compact,
|
|
178
|
+
tail: true,
|
|
179
|
+
accountId,
|
|
180
|
+
appId,
|
|
181
|
+
systemType,
|
|
182
|
+
typeName: SYSTEM_TYPE_DISPLAY_NAMES[systemType],
|
|
183
|
+
});
|
|
184
|
+
if (data.paging?.next?.after) {
|
|
185
|
+
currentAfter = data.paging.next.after;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const lastResult = data.results[data.results.length - 1];
|
|
189
|
+
currentStartTime = lastResult.requestExecutionTimestamp + 1;
|
|
190
|
+
currentAfter = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
timeoutHandle = setTimeout(() => {
|
|
194
|
+
tail();
|
|
195
|
+
}, TAIL_DELAY);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
await onTerminate();
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const tailCopy = commands.app.subcommands.logs.outputMessages.tailMessages;
|
|
203
|
+
SpinniesManager.add('tailLogs', {
|
|
204
|
+
text: tailCopy.following(appId),
|
|
205
|
+
});
|
|
206
|
+
SpinniesManager.add('stopMessage', {
|
|
207
|
+
text: tailCopy.stop,
|
|
208
|
+
status: 'non-spinnable',
|
|
209
|
+
});
|
|
210
|
+
void tail();
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
export const handleLogDetailsRequest = async (accountId, appId, logId, systemType, options) => {
|
|
214
|
+
const { json } = options;
|
|
215
|
+
const { data } = await getAppLogDetails(accountId, appId, systemType, logId);
|
|
216
|
+
if (json) {
|
|
217
|
+
uiLogger.json(data);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
outputAppLogDetails(data.log, {
|
|
221
|
+
accountId,
|
|
222
|
+
appId,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|