@hubspot/cli 8.8.0 → 8.9.0-beta.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/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/lib/app/urls.d.ts
CHANGED
|
@@ -14,4 +14,6 @@ type PublicAppInstallUrlArgs = {
|
|
|
14
14
|
export declare function getOauthAppInstallUrl({ targetAccountId, env, clientId, scopes, redirectUrls, }: PublicAppInstallUrlArgs): string;
|
|
15
15
|
export declare function getStaticAuthAppInstallUrl({ targetAccountId, env, appId, }: PrivateAppInstallUrlArgs): string;
|
|
16
16
|
export declare function getAppCardSetupUrl({ targetAccountId, env, appId, }: PrivateAppInstallUrlArgs): string;
|
|
17
|
+
export declare function getAppLogsUrl(accountId: number, appId: number, systemType: string): string;
|
|
18
|
+
export declare function getAppLogDetailsUrl(accountId: number, appId: number, systemType: string, logId: string): string;
|
|
17
19
|
export {};
|
package/lib/app/urls.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
2
|
+
import { getBaseHubSpotUrlForAccount } from '../projects/urls.js';
|
|
2
3
|
export function getOauthAppInstallUrl({ targetAccountId, env, clientId, scopes, redirectUrls, }) {
|
|
3
4
|
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
4
5
|
return (`${websiteOrigin}/oauth/${targetAccountId}/authorize` +
|
|
@@ -14,3 +15,9 @@ export function getAppCardSetupUrl({ targetAccountId, env, appId, }) {
|
|
|
14
15
|
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
15
16
|
return `${websiteOrigin}/integrations-settings/${targetAccountId}/installed/framework/${appId}/app-cards?tourId=get-started`;
|
|
16
17
|
}
|
|
18
|
+
export function getAppLogsUrl(accountId, appId, systemType) {
|
|
19
|
+
return `${getBaseHubSpotUrlForAccount(accountId)}/developer-monitoring/${accountId}/?logType=${systemType}&appId=${appId}`;
|
|
20
|
+
}
|
|
21
|
+
export function getAppLogDetailsUrl(accountId, appId, systemType, logId) {
|
|
22
|
+
return `${getAppLogsUrl(accountId, appId, systemType)}&logId=${logId}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
5
|
+
import CLIWebSocketServer from '../CLIWebSocketServer.js';
|
|
6
|
+
import { ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES, ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES, ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION, } from '../constants.js';
|
|
7
|
+
import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
|
|
8
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
9
|
+
import { uiLogger } from '../ui/logger.js';
|
|
10
|
+
import { lib } from '../../lang/en.js';
|
|
11
|
+
import { PromptExitError } from '../errors/PromptExitError.js';
|
|
12
|
+
import { EXIT_CODES } from '../enums/exitCodes.js';
|
|
13
|
+
const LOG_PREFIX = '[AccountAuthWebsocketServer]';
|
|
14
|
+
const SPINNER_ID = 'pak-websocket';
|
|
15
|
+
const CTRL_C = 3;
|
|
16
|
+
function isPersonalAccessKeyMessageData(data) {
|
|
17
|
+
if (typeof data !== 'object' || data === null)
|
|
18
|
+
return false;
|
|
19
|
+
const d = data;
|
|
20
|
+
return (typeof d.personalAccessKey === 'string' &&
|
|
21
|
+
typeof d.cliCallbackToken === 'string');
|
|
22
|
+
}
|
|
23
|
+
function buildPakUrl(env, account, cliCallbackPort, cliCallbackToken) {
|
|
24
|
+
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
25
|
+
const baseUrl = account
|
|
26
|
+
? `${websiteOrigin}/personal-access-key/${account}`
|
|
27
|
+
: `${websiteOrigin}/l/personal-access-key`;
|
|
28
|
+
return `${baseUrl}?cliCallbackPort=${cliCallbackPort}&cliCallbackToken=${cliCallbackToken}`;
|
|
29
|
+
}
|
|
30
|
+
function createMessageHandler(server, cliCallbackToken, resolveWebsocketPak) {
|
|
31
|
+
return (ws, message) => {
|
|
32
|
+
if (message.type !== ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES.PERSONAL_ACCESS_KEY) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!isPersonalAccessKeyMessageData(message.data)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const { cliCallbackToken: receivedToken, personalAccessKey } = message.data;
|
|
39
|
+
if (receivedToken !== cliCallbackToken) {
|
|
40
|
+
server.sendMessage(ws, {
|
|
41
|
+
type: ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES.AUTH_FAILED,
|
|
42
|
+
data: { reason: 'cliCallbackTokenMismatch' },
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (personalAccessKey.length === 0) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
server.sendMessage(ws, {
|
|
50
|
+
type: ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES.AUTH_RECEIVED,
|
|
51
|
+
});
|
|
52
|
+
resolveWebsocketPak(personalAccessKey);
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function awaitKeypressOrWebsocketPak(websocketPakPromise) {
|
|
57
|
+
if (!process.stdin.isTTY) {
|
|
58
|
+
return { type: 'keypress' };
|
|
59
|
+
}
|
|
60
|
+
SpinniesManager.init();
|
|
61
|
+
SpinniesManager.add(SPINNER_ID, {
|
|
62
|
+
text: lib.accountAuthWebsocket.logs.spinner,
|
|
63
|
+
});
|
|
64
|
+
let cleanupStdin;
|
|
65
|
+
const keypressPromise = new Promise((resolve, reject) => {
|
|
66
|
+
readline.emitKeypressEvents(process.stdin);
|
|
67
|
+
process.stdin.setRawMode(true);
|
|
68
|
+
const onData = (chunk) => {
|
|
69
|
+
doCleanup();
|
|
70
|
+
if (chunk[0] === CTRL_C) {
|
|
71
|
+
return reject(new PromptExitError(lib.prompts.personalAccessKeyPrompt.errors.authCancelled, EXIT_CODES.SUCCESS));
|
|
72
|
+
}
|
|
73
|
+
resolve({ type: 'keypress' });
|
|
74
|
+
};
|
|
75
|
+
const doCleanup = () => {
|
|
76
|
+
process.stdin.removeListener('data', onData);
|
|
77
|
+
try {
|
|
78
|
+
process.stdin.setRawMode(false);
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
process.stdin.pause();
|
|
82
|
+
};
|
|
83
|
+
cleanupStdin = doCleanup;
|
|
84
|
+
process.stdin.on('data', onData);
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const result = await Promise.race([
|
|
88
|
+
websocketPakPromise.then(pak => ({ type: 'pak', pak })),
|
|
89
|
+
keypressPromise,
|
|
90
|
+
]);
|
|
91
|
+
if (result.type === 'pak') {
|
|
92
|
+
SpinniesManager.succeed(SPINNER_ID, {
|
|
93
|
+
text: lib.accountAuthWebsocket.logs.received,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
SpinniesManager.remove(SPINNER_ID);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
SpinniesManager.remove(SPINNER_ID);
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
cleanupStdin?.();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function awaitPersonalAccessKeyOverWebsocket({ env, account, }) {
|
|
110
|
+
const cliCallbackToken = randomUUID();
|
|
111
|
+
const server = new CLIWebSocketServer({ logPrefix: LOG_PREFIX });
|
|
112
|
+
let resolveWebsocketPak;
|
|
113
|
+
const websocketPakPromise = new Promise(resolve => {
|
|
114
|
+
resolveWebsocketPak = resolve;
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const cliCallbackPort = await server.start({
|
|
118
|
+
metadata: {
|
|
119
|
+
accountAuthWebsocketServerVersion: ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION,
|
|
120
|
+
},
|
|
121
|
+
onMessage: createMessageHandler(server, cliCallbackToken, resolveWebsocketPak),
|
|
122
|
+
});
|
|
123
|
+
const url = buildPakUrl(env, account, cliCallbackPort, cliCallbackToken);
|
|
124
|
+
await open(url, { url: true });
|
|
125
|
+
uiLogger.log(lib.accountAuthWebsocket.logs.openingWebBrowser(url));
|
|
126
|
+
const waitResult = await awaitKeypressOrWebsocketPak(websocketPakPromise);
|
|
127
|
+
if (waitResult.type === 'pak') {
|
|
128
|
+
return waitResult.pak;
|
|
129
|
+
}
|
|
130
|
+
let pastePhaseActive = true;
|
|
131
|
+
websocketPakPromise
|
|
132
|
+
.then(pak => {
|
|
133
|
+
if (pastePhaseActive) {
|
|
134
|
+
process.stdin.push(`${pak}\n`);
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.catch(() => { });
|
|
138
|
+
const personalAccessKey = await personalAccessKeyPrompt();
|
|
139
|
+
pastePhaseActive = false;
|
|
140
|
+
return personalAccessKey;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
server.shutdown();
|
|
144
|
+
}
|
|
145
|
+
}
|
package/lib/buildAccount.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createDeveloperTestAccount, fetchDeveloperTestAccountGateSyncStatus, ge
|
|
|
5
5
|
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
|
|
6
6
|
import { createV2Sandbox, getSandboxPersonalAccessKey, } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
7
7
|
import { isPromptExitError } from './errors/PromptExitError.js';
|
|
8
|
-
import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
|
|
8
|
+
import { legacyPersonalAccessKeyPrompt as personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
|
|
9
9
|
import { createDeveloperTestAccountConfigPrompt } from './prompts/createDeveloperTestAccountConfigPrompt.js';
|
|
10
10
|
import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
|
|
11
11
|
import SpinniesManager from './ui/SpinniesManager.js';
|
package/lib/constants.d.ts
CHANGED
|
@@ -124,6 +124,14 @@ export declare const LOCAL_DEV_SERVER_MESSAGE_TYPES: {
|
|
|
124
124
|
readonly OAUTH_APP_INSTALL_INITIATED: "APP_INSTALL_INITIATED";
|
|
125
125
|
};
|
|
126
126
|
export declare const LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID = "local-dev-ui-websocket-server";
|
|
127
|
+
export declare const ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES: {
|
|
128
|
+
readonly AUTH_RECEIVED: "server:authReceived";
|
|
129
|
+
readonly AUTH_FAILED: "server:authFailed";
|
|
130
|
+
};
|
|
131
|
+
export declare const ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES: {
|
|
132
|
+
readonly PERSONAL_ACCESS_KEY: "client:personalAccessKey";
|
|
133
|
+
};
|
|
134
|
+
export declare const ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION = 1;
|
|
127
135
|
export declare const CONFIG_LOCAL_STATE_FLAGS: {
|
|
128
136
|
readonly LOCAL_DEV_UI_WELCOME: "LOCAL_DEV_UI_WELCOME";
|
|
129
137
|
};
|
package/lib/constants.js
CHANGED
|
@@ -116,6 +116,14 @@ export const LOCAL_DEV_SERVER_MESSAGE_TYPES = {
|
|
|
116
116
|
OAUTH_APP_INSTALL_INITIATED: 'APP_INSTALL_INITIATED',
|
|
117
117
|
};
|
|
118
118
|
export const LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID = 'local-dev-ui-websocket-server';
|
|
119
|
+
export const ACCOUNT_AUTH_UI_MESSAGE_SEND_TYPES = {
|
|
120
|
+
AUTH_RECEIVED: 'server:authReceived',
|
|
121
|
+
AUTH_FAILED: 'server:authFailed',
|
|
122
|
+
};
|
|
123
|
+
export const ACCOUNT_AUTH_UI_MESSAGE_RECEIVE_TYPES = {
|
|
124
|
+
PERSONAL_ACCESS_KEY: 'client:personalAccessKey',
|
|
125
|
+
};
|
|
126
|
+
export const ACCOUNT_AUTH_WEBSOCKET_SERVER_VERSION = 1;
|
|
119
127
|
export const CONFIG_LOCAL_STATE_FLAGS = {
|
|
120
128
|
LOCAL_DEV_UI_WELCOME: 'LOCAL_DEV_UI_WELCOME',
|
|
121
129
|
};
|
|
@@ -21,6 +21,7 @@ const SKIP_CONFIG_LOADING_COMMANDS = {
|
|
|
21
21
|
init: true,
|
|
22
22
|
feedback: true,
|
|
23
23
|
mcp: { start: true },
|
|
24
|
+
project: { lint: true },
|
|
24
25
|
};
|
|
25
26
|
// Returns true if the command requires a config file to be present
|
|
26
27
|
export function shouldLoadConfigForCommand(commandParts) {
|
|
@@ -150,7 +150,7 @@ class LocalDevWebsocketServer {
|
|
|
150
150
|
this.setupDevServersStartedListener(websocket);
|
|
151
151
|
}
|
|
152
152
|
async start() {
|
|
153
|
-
|
|
153
|
+
await this.cliWebSocketServer.start({
|
|
154
154
|
metadata: {
|
|
155
155
|
localDevWebsocketServerVersion: LOCAL_DEV_WEBSOCKET_SERVER_VERSION,
|
|
156
156
|
},
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import archiver from 'archiver';
|
|
2
|
-
import { WorkspaceMapping, FileDependencyMapping } from '@hubspot/project-parsing-lib/workspaces';
|
|
2
|
+
import { WorkspaceMapping, FileDependencyMapping, FileDependencyKind, LocalDependencyProtocol } from '@hubspot/project-parsing-lib/workspaces';
|
|
3
|
+
export type FileDepArchiveEntry = {
|
|
4
|
+
archivePath: string;
|
|
5
|
+
protocol: LocalDependencyProtocol;
|
|
6
|
+
};
|
|
3
7
|
/**
|
|
4
8
|
* Result of archiving workspaces and file dependencies
|
|
5
9
|
*/
|
|
6
10
|
export type WorkspaceArchiveResult = {
|
|
7
11
|
packageWorkspaces: Map<string, string[]>;
|
|
8
|
-
packageFileDeps: Map<string, Map<string,
|
|
12
|
+
packageFileDeps: Map<string, Map<string, FileDepArchiveEntry>>;
|
|
9
13
|
};
|
|
10
14
|
/**
|
|
11
15
|
* Generates a short hash of the input string for use in workspace paths.
|
|
@@ -24,10 +28,14 @@ export declare function shortHash(input: string): string;
|
|
|
24
28
|
export declare function toPosixPath(p: string): string;
|
|
25
29
|
/**
|
|
26
30
|
* Determines the archive path for an external workspace or file: dependency.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
*
|
|
32
|
+
* For directories, produces `_workspaces/<basename>-<hash>`.
|
|
33
|
+
* For tarballs, produces `_workspaces/<basename-no-ext>-<hash>/<original-basename>`,
|
|
34
|
+
* so the rewritten package.json reference still ends in the original filename.
|
|
35
|
+
*
|
|
36
|
+
* The hash prevents collisions between different paths with the same basename.
|
|
29
37
|
*/
|
|
30
|
-
export declare function computeExternalArchivePath(absolutePath: string): string;
|
|
38
|
+
export declare function computeExternalArchivePath(absolutePath: string, kind?: FileDependencyKind): string;
|
|
31
39
|
/**
|
|
32
40
|
* Updates package.json files in the archive to reflect new workspace and file: dependency paths.
|
|
33
41
|
*
|
|
@@ -38,7 +46,7 @@ export declare function computeExternalArchivePath(absolutePath: string): string
|
|
|
38
46
|
* Only external file: dependencies appear in packageFileDeps; internal ones
|
|
39
47
|
* keep their original file: references and are left untouched.
|
|
40
48
|
*/
|
|
41
|
-
export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string,
|
|
49
|
+
export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string, FileDepArchiveEntry>>): Promise<void>;
|
|
42
50
|
export declare function rewriteLockfileForExternalDeps(lockfileContent: Record<string, unknown>, pathMappings: Array<{
|
|
43
51
|
oldPath: string;
|
|
44
52
|
newPath: string;
|
|
@@ -5,6 +5,10 @@ import { shouldIgnoreFile } from '@hubspot/local-dev-lib/ignoreRules';
|
|
|
5
5
|
import { getPackableFiles, } from '@hubspot/project-parsing-lib/workspaces';
|
|
6
6
|
import { uiLogger } from '../ui/logger.js';
|
|
7
7
|
import { lib } from '../../lang/en.js';
|
|
8
|
+
const FILE_PROTOCOL_PREFIX = 'file:';
|
|
9
|
+
const LINK_PROTOCOL_PREFIX = 'link:';
|
|
10
|
+
const KIND_DIRECTORY = 'directory';
|
|
11
|
+
const KIND_TARBALL = 'tarball';
|
|
8
12
|
/**
|
|
9
13
|
* Generates a short hash of the input string for use in workspace paths.
|
|
10
14
|
* Uses SHA256 truncated to 8 hex characters (4 billion possibilities).
|
|
@@ -27,14 +31,35 @@ export function toPosixPath(p) {
|
|
|
27
31
|
}
|
|
28
32
|
return p.replaceAll(path.sep, path.posix.sep);
|
|
29
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Strips the longest matching tarball extension (.tar.gz, .tgz, .tar)
|
|
36
|
+
* from a file basename. Returns the input unchanged if no extension matches.
|
|
37
|
+
*/
|
|
38
|
+
function stripTarballExtension(basename) {
|
|
39
|
+
const lower = basename.toLowerCase();
|
|
40
|
+
for (const ext of ['.tar.gz', '.tgz', '.tar']) {
|
|
41
|
+
if (lower.endsWith(ext)) {
|
|
42
|
+
return basename.slice(0, basename.length - ext.length);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return basename;
|
|
46
|
+
}
|
|
30
47
|
/**
|
|
31
48
|
* Determines the archive path for an external workspace or file: dependency.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* For directories, produces `_workspaces/<basename>-<hash>`.
|
|
51
|
+
* For tarballs, produces `_workspaces/<basename-no-ext>-<hash>/<original-basename>`,
|
|
52
|
+
* so the rewritten package.json reference still ends in the original filename.
|
|
53
|
+
*
|
|
54
|
+
* The hash prevents collisions between different paths with the same basename.
|
|
34
55
|
*/
|
|
35
|
-
export function computeExternalArchivePath(absolutePath) {
|
|
56
|
+
export function computeExternalArchivePath(absolutePath, kind = KIND_DIRECTORY) {
|
|
36
57
|
const resolved = path.resolve(absolutePath);
|
|
37
58
|
const name = path.basename(resolved);
|
|
59
|
+
if (kind === KIND_TARBALL) {
|
|
60
|
+
const nameNoExt = stripTarballExtension(name);
|
|
61
|
+
return path.posix.join('_workspaces', `${nameNoExt}-${shortHash(resolved)}`, name);
|
|
62
|
+
}
|
|
38
63
|
return path.posix.join('_workspaces', `${name}-${shortHash(resolved)}`);
|
|
39
64
|
}
|
|
40
65
|
/**
|
|
@@ -121,25 +146,25 @@ async function archiveWorkspaceDirectories(archive, srcDir, workspaceMappings) {
|
|
|
121
146
|
return { externalArchivePaths, packageWorkspaceEntries };
|
|
122
147
|
}
|
|
123
148
|
/**
|
|
124
|
-
* Archives file: dependencies and returns mapping information.
|
|
149
|
+
* Archives file: and link: dependencies and returns mapping information.
|
|
125
150
|
*
|
|
126
|
-
* Internal
|
|
127
|
-
*
|
|
151
|
+
* Internal dependencies (inside srcDir) are skipped — their original
|
|
152
|
+
* references in package.json remain valid after upload.
|
|
128
153
|
*
|
|
129
|
-
* External
|
|
130
|
-
*
|
|
154
|
+
* External directory dependencies are archived to `_workspaces/<name>-<hash>`.
|
|
155
|
+
* External tarball dependencies are archived to
|
|
156
|
+
* `_workspaces/<name-no-ext>-<hash>/<original-basename>` so the rewritten
|
|
157
|
+
* reference still ends in the original filename.
|
|
131
158
|
*/
|
|
132
159
|
async function archiveFileDependencies(archive, srcDir, fileDependencyMappings, externalArchivePaths) {
|
|
133
160
|
const packageFileDeps = new Map();
|
|
134
161
|
const toArchive = [];
|
|
135
162
|
for (const mapping of fileDependencyMappings) {
|
|
136
|
-
const { packageName, localPath, sourcePackageJsonPath } = mapping;
|
|
163
|
+
const { packageName, localPath, sourcePackageJsonPath, kind, protocol } = mapping;
|
|
137
164
|
if (isInsideSrcDir(localPath, srcDir)) {
|
|
138
|
-
// Internal: original file: reference stays unchanged, nothing to do
|
|
139
165
|
continue;
|
|
140
166
|
}
|
|
141
|
-
|
|
142
|
-
const archivePath = computeExternalArchivePath(localPath);
|
|
167
|
+
const archivePath = computeExternalArchivePath(localPath, kind);
|
|
143
168
|
const resolvedPath = path.resolve(localPath);
|
|
144
169
|
if (!packageFileDeps.has(sourcePackageJsonPath)) {
|
|
145
170
|
packageFileDeps.set(sourcePackageJsonPath, new Map());
|
|
@@ -148,23 +173,27 @@ async function archiveFileDependencies(archive, srcDir, fileDependencyMappings,
|
|
|
148
173
|
const relativeArchivePath = toPosixPath(path.relative(relPkgJsonDir, archivePath));
|
|
149
174
|
packageFileDeps
|
|
150
175
|
.get(sourcePackageJsonPath)
|
|
151
|
-
.set(packageName, relativeArchivePath);
|
|
152
|
-
// Only archive each unique path once
|
|
176
|
+
.set(packageName, { archivePath: relativeArchivePath, protocol });
|
|
153
177
|
if (!externalArchivePaths.has(resolvedPath)) {
|
|
154
178
|
externalArchivePaths.set(resolvedPath, archivePath);
|
|
155
|
-
toArchive.push({ localPath, archivePath, packageName });
|
|
179
|
+
toArchive.push({ localPath, archivePath, packageName, kind });
|
|
156
180
|
}
|
|
157
181
|
}
|
|
158
|
-
|
|
159
|
-
const
|
|
182
|
+
const directoryItems = toArchive.filter(item => item.kind === KIND_DIRECTORY);
|
|
183
|
+
const tarballItems = toArchive.filter(item => item.kind === KIND_TARBALL);
|
|
184
|
+
// getPackableFiles only applies to directory deps; tarballs are a single file.
|
|
185
|
+
const directoriesWithPackableFiles = await Promise.all(directoryItems.map(async (item) => ({
|
|
160
186
|
...item,
|
|
161
187
|
packableFiles: await getPackableFiles(item.localPath),
|
|
162
188
|
})));
|
|
163
|
-
|
|
164
|
-
for (const { localPath, archivePath, packageName, packableFiles, } of withPackableFiles) {
|
|
189
|
+
for (const { localPath, archivePath, packageName, packableFiles, } of directoriesWithPackableFiles) {
|
|
165
190
|
uiLogger.log(lib.projectUpload.handleProjectUpload.fileDependencyIncluded(packageName, localPath, archivePath));
|
|
166
191
|
archive.directory(localPath, archivePath, createWorkspaceFileFilter(packableFiles));
|
|
167
192
|
}
|
|
193
|
+
for (const { localPath, archivePath, packageName } of tarballItems) {
|
|
194
|
+
uiLogger.log(lib.projectUpload.handleProjectUpload.fileDependencyIncluded(packageName, localPath, archivePath));
|
|
195
|
+
archive.file(localPath, { name: archivePath });
|
|
196
|
+
}
|
|
168
197
|
return packageFileDeps;
|
|
169
198
|
}
|
|
170
199
|
/**
|
|
@@ -213,14 +242,19 @@ export async function updatePackageJsonInArchive(archive, srcDir, packageWorkspa
|
|
|
213
242
|
uiLogger.debug(lib.projectUpload.handleProjectUpload.updatingPackageJsonWorkspaces(relativePackageJsonPath));
|
|
214
243
|
uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedWorkspaces(workspaceEntries.join(', ')));
|
|
215
244
|
}
|
|
216
|
-
// Update external file: dependencies; internal ones are left untouched
|
|
245
|
+
// Update external file: and link: dependencies; internal ones are left untouched.
|
|
246
|
+
// The protocol prefix (file: vs link:) is preserved from the original spec.
|
|
217
247
|
const fileDeps = packageFileDeps.get(packageJsonPath);
|
|
218
248
|
if (fileDeps && fileDeps.size > 0 && packageJson.dependencies) {
|
|
219
|
-
for (const [packageName, archivePath] of fileDeps.entries()) {
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
for (const [packageName, { archivePath, protocol },] of fileDeps.entries()) {
|
|
250
|
+
const current = packageJson.dependencies[packageName];
|
|
251
|
+
if (typeof current === 'string' &&
|
|
252
|
+
(current.startsWith(FILE_PROTOCOL_PREFIX) ||
|
|
253
|
+
current.startsWith(LINK_PROTOCOL_PREFIX))) {
|
|
254
|
+
const newValue = `${protocol}:${archivePath}`;
|
|
255
|
+
packageJson.dependencies[packageName] = newValue;
|
|
222
256
|
modified = true;
|
|
223
|
-
uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedFileDependency(packageName,
|
|
257
|
+
uiLogger.debug(lib.projectUpload.handleProjectUpload.updatedFileDependency(packageName, newValue));
|
|
224
258
|
}
|
|
225
259
|
}
|
|
226
260
|
}
|
|
@@ -248,11 +282,23 @@ export function rewriteLockfileForExternalDeps(lockfileContent, pathMappings) {
|
|
|
248
282
|
typeof value === 'object' &&
|
|
249
283
|
value !== null) {
|
|
250
284
|
const entry = value;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
285
|
+
if (typeof entry.resolved !== 'string')
|
|
286
|
+
continue;
|
|
287
|
+
// Symlink entries (directory deps with link:true) store resolved as a
|
|
288
|
+
// bare relative path. Tarball entries store resolved as a "file:" URL.
|
|
289
|
+
const resolved = entry.resolved;
|
|
290
|
+
const isFileUrl = resolved.startsWith(FILE_PROTOCOL_PREFIX);
|
|
291
|
+
const resolvedPath = isFileUrl
|
|
292
|
+
? resolved.slice(FILE_PROTOCOL_PREFIX.length)
|
|
293
|
+
: resolved;
|
|
294
|
+
const mapping = pathMappings.find(m => m.oldPath === resolvedPath);
|
|
295
|
+
if (mapping) {
|
|
296
|
+
newPackages[key] = {
|
|
297
|
+
...entry,
|
|
298
|
+
resolved: isFileUrl
|
|
299
|
+
? `${FILE_PROTOCOL_PREFIX}${mapping.newPath}`
|
|
300
|
+
: mapping.newPath,
|
|
301
|
+
};
|
|
256
302
|
}
|
|
257
303
|
}
|
|
258
304
|
}
|
|
@@ -17,13 +17,10 @@ type ScopesPromptResponse = {
|
|
|
17
17
|
scopes: string[];
|
|
18
18
|
};
|
|
19
19
|
export type OauthPromptResponse = AccountNamePromptResponse & AccountIdPromptResponse & ClientIdPromptResponse & ClientSecretPromptResponse & ScopesPromptResponse;
|
|
20
|
-
|
|
21
|
-
* Displays notification to user that we are about to open the browser,
|
|
22
|
-
* then opens their browser to the personal-access-key shortlink
|
|
23
|
-
*/
|
|
24
|
-
export declare function personalAccessKeyPrompt({ env, account, }: {
|
|
20
|
+
export declare function legacyPersonalAccessKeyPrompt({ env, account, }: {
|
|
25
21
|
env: string;
|
|
26
22
|
account?: number;
|
|
27
23
|
}): Promise<PersonalAccessKeyPromptResponse>;
|
|
24
|
+
export declare function personalAccessKeyPrompt(): Promise<string>;
|
|
28
25
|
export declare const OAUTH_FLOW: (PromptConfig<AccountNamePromptResponse> | PromptConfig<AccountIdPromptResponse> | PromptConfig<ClientIdPromptResponse> | PromptConfig<ClientSecretPromptResponse> | PromptConfig<ScopesPromptResponse>)[];
|
|
29
26
|
export {};
|
|
@@ -9,11 +9,8 @@ import { uiInfoSection } from '../ui/index.js';
|
|
|
9
9
|
import { lib } from '../../lang/en.js';
|
|
10
10
|
import { PromptExitError } from '../errors/PromptExitError.js';
|
|
11
11
|
import { EXIT_CODES } from '../enums/exitCodes.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* then opens their browser to the personal-access-key shortlink
|
|
15
|
-
*/
|
|
16
|
-
export async function personalAccessKeyPrompt({ env, account, }) {
|
|
12
|
+
// Full PAK flow: browser-open menu + paste. Used by commands that haven't adopted websocket PAK delivery.
|
|
13
|
+
export async function legacyPersonalAccessKeyPrompt({ env, account, }) {
|
|
17
14
|
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
18
15
|
let url = `${websiteOrigin}/l/personal-access-key`;
|
|
19
16
|
if (process.env.BROWSER !== 'none') {
|
|
@@ -43,6 +40,11 @@ export async function personalAccessKeyPrompt({ env, account, }) {
|
|
|
43
40
|
env,
|
|
44
41
|
};
|
|
45
42
|
}
|
|
43
|
+
// Paste-only: shows the masked input directly. Used by hs account auth after the browser is already open via websocket.
|
|
44
|
+
export async function personalAccessKeyPrompt() {
|
|
45
|
+
const { personalAccessKey } = await promptUser(PERSONAL_ACCESS_KEY);
|
|
46
|
+
return personalAccessKey;
|
|
47
|
+
}
|
|
46
48
|
const ACCOUNT_ID = {
|
|
47
49
|
name: 'accountId',
|
|
48
50
|
message: lib.prompts.personalAccessKeyPrompt.enterAccountId,
|
|
@@ -27,6 +27,7 @@ export async function selectAppPrompt(accountId, appId) {
|
|
|
27
27
|
uiLogger.error(lib.prompts.selectAppPrompt.errors.invalidAppId);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
availableApps.sort((a, b) => a.name.localeCompare(b.name));
|
|
30
31
|
const appPromptValue = await listPrompt(lib.prompts.selectAppPrompt.selectAppId, {
|
|
31
32
|
choices: availableApps.map(app => ({
|
|
32
33
|
name: `${app.name} (${app.id})`,
|
|
@@ -11,7 +11,8 @@ export async function setAsDefaultAccountPrompt(accountName) {
|
|
|
11
11
|
name: 'setAsDefault',
|
|
12
12
|
type: 'confirm',
|
|
13
13
|
when: accounts.length >= 1 && defaultAccount?.name !== accountName,
|
|
14
|
-
message: lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccountMessage,
|
|
14
|
+
message: lib.prompts.setAsDefaultAccountPrompt.setAsDefaultAccountMessage(accountName),
|
|
15
|
+
default: false,
|
|
15
16
|
},
|
|
16
17
|
]);
|
|
17
18
|
uiLogger.log('');
|
package/lib/serverlessLogs.js
CHANGED
|
@@ -2,11 +2,11 @@ import https from 'https';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { uiLogger } from './ui/logger.js';
|
|
4
4
|
import { isHubSpotHttpError, isMissingScopeError, } from '@hubspot/local-dev-lib/errors/index';
|
|
5
|
-
import {
|
|
5
|
+
import { PERSONAL_ACCESS_KEY_AUTH_METHOD, SCOPE_GROUPS, } from '@hubspot/local-dev-lib/constants/auth';
|
|
6
6
|
import { getConfigAccountById } from '@hubspot/local-dev-lib/config';
|
|
7
7
|
import { fetchScopeData } from '@hubspot/local-dev-lib/api/localDevAuth';
|
|
8
8
|
import { outputLogs } from './ui/serverlessFunctionLogs.js';
|
|
9
|
-
import {
|
|
9
|
+
import { ApiErrorContext, logError } from './errorHandlers/index.js';
|
|
10
10
|
import SpinniesManager from './ui/SpinniesManager.js';
|
|
11
11
|
import { handleExit, handleKeypress } from './process.js';
|
|
12
12
|
import { lib } from '../lang/en.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AppLogDetails } from '@hubspot/local-dev-lib/types/AppLogs';
|
|
2
|
+
type AppLogEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
executionTimeMillis?: number;
|
|
6
|
+
portalId?: number;
|
|
7
|
+
traceId?: string;
|
|
8
|
+
status: 'SUCCESS' | 'ERROR';
|
|
9
|
+
errorType?: string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
};
|
|
12
|
+
type AppLogsSearchResponse = {
|
|
13
|
+
results: AppLogEntry[];
|
|
14
|
+
hasMore: boolean;
|
|
15
|
+
offset: number;
|
|
16
|
+
total: number;
|
|
17
|
+
};
|
|
18
|
+
type OutputOptions = {
|
|
19
|
+
compact?: boolean;
|
|
20
|
+
tail?: boolean;
|
|
21
|
+
accountId: number;
|
|
22
|
+
appId: number;
|
|
23
|
+
systemType: string;
|
|
24
|
+
typeName: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function outputAppLogs(response: AppLogsSearchResponse, options: OutputOptions): Promise<void>;
|
|
27
|
+
type LogDetailsOutputOptions = {
|
|
28
|
+
accountId: number;
|
|
29
|
+
appId: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function outputAppLogDetails(details: AppLogDetails, options: LogDetailsOutputOptions): void;
|
|
32
|
+
export {};
|