@hubspot/cli 8.0.10-experimental.2 → 8.0.10-experimental.3
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.js +15 -5
- package/commands/account/use.js +14 -4
- package/commands/auth.js +10 -6
- package/commands/cms/theme/preview.js +9 -64
- package/commands/hubdb/clear.js +4 -0
- package/commands/hubdb/delete.js +4 -0
- package/commands/hubdb/fetch.js +4 -0
- package/commands/init.js +4 -0
- package/commands/project/dev/index.js +29 -19
- package/commands/project/download.js +5 -1
- package/commands/sandbox/__tests__/create.test.js +1 -48
- package/commands/sandbox/create.js +3 -30
- package/commands/testAccount/create.js +4 -0
- package/lang/en.d.ts +11 -0
- package/lang/en.js +11 -0
- package/lib/__tests__/buildAccount.test.js +1 -52
- package/lib/__tests__/sandboxes.test.js +1 -29
- package/lib/__tests__/serverlessLogs.test.js +10 -1
- package/lib/accountAuth.js +4 -0
- package/lib/buildAccount.d.ts +1 -6
- package/lib/buildAccount.js +9 -42
- package/lib/constants.d.ts +0 -2
- package/lib/constants.js +0 -2
- package/lib/errors/PromptExitError.d.ts +4 -0
- package/lib/errors/PromptExitError.js +8 -0
- package/lib/projects/__tests__/components.test.js +14 -0
- package/lib/projects/components.js +12 -2
- package/lib/projects/localDev/AppDevModeInterface.js +4 -0
- package/lib/projects/localDev/LocalDevManager_DEPRECATED.js +4 -0
- package/lib/projects/localDev/helpers/account.js +5 -11
- package/lib/prompts/downloadProjectPrompt.js +11 -10
- package/lib/prompts/installAppPrompt.js +3 -2
- package/lib/prompts/personalAccessKeyPrompt.js +3 -2
- package/lib/prompts/projectDevTargetAccountPrompt.js +13 -16
- package/lib/prompts/selectHubDBTablePrompt.js +8 -4
- package/lib/prompts/selectPublicAppForMigrationPrompt.js +12 -6
- package/lib/sandboxes.d.ts +1 -9
- package/lib/sandboxes.js +0 -21
- package/lib/theme/cmsDevServerProcess.d.ts +12 -0
- package/lib/theme/cmsDevServerProcess.js +148 -0
- package/lib/theme/cmsDevServerRunner.d.ts +14 -0
- package/lib/theme/cmsDevServerRunner.js +90 -0
- package/lib/usageTracking.js +8 -5
- package/package.json +2 -3
- package/lib/__tests__/sandboxSync.test.d.ts +0 -1
- package/lib/__tests__/sandboxSync.test.js +0 -147
- package/lib/sandboxSync.d.ts +0 -4
- package/lib/sandboxSync.js +0 -102
package/lib/sandboxes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
|
|
2
|
-
import { fetchTypes } from '@hubspot/local-dev-lib/api/sandboxSync';
|
|
3
2
|
import { getAllConfigAccounts } from '@hubspot/local-dev-lib/config';
|
|
4
3
|
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
5
4
|
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
|
|
@@ -8,19 +7,12 @@ import { uiLogger } from './ui/logger.js';
|
|
|
8
7
|
import { lib } from '../lang/en.js';
|
|
9
8
|
import { logError } from './errorHandlers/index.js';
|
|
10
9
|
import { uiAccountDescription } from './ui/index.js';
|
|
11
|
-
export const SYNC_TYPES = {
|
|
12
|
-
OBJECT_RECORDS: 'object-records',
|
|
13
|
-
};
|
|
14
10
|
export const SANDBOX_TYPE_MAP = {
|
|
15
11
|
dev: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
|
|
16
12
|
developer: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
|
|
17
13
|
development: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
|
|
18
14
|
standard: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX,
|
|
19
15
|
};
|
|
20
|
-
export const SANDBOX_API_TYPE_MAP = {
|
|
21
|
-
[HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 1,
|
|
22
|
-
[HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 2,
|
|
23
|
-
};
|
|
24
16
|
export const SANDBOX_TYPE_MAP_V2 = {
|
|
25
17
|
[HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 'STANDARD',
|
|
26
18
|
[HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 'DEVELOPER',
|
|
@@ -45,19 +37,6 @@ export function getHasSandboxesByType(parentAccountConfig, type) {
|
|
|
45
37
|
}
|
|
46
38
|
return false;
|
|
47
39
|
}
|
|
48
|
-
// Fetches available sync types for a given sandbox portal
|
|
49
|
-
export async function getAvailableSyncTypes(parentAccountConfig, config) {
|
|
50
|
-
const parentPortalId = parentAccountConfig.accountId;
|
|
51
|
-
const portalId = config.accountId;
|
|
52
|
-
if (!parentPortalId || !portalId) {
|
|
53
|
-
throw new Error(lib.sandbox.sync.failure.syncTypeFetch);
|
|
54
|
-
}
|
|
55
|
-
const { data: { results: syncTypes }, } = await fetchTypes(parentPortalId, portalId);
|
|
56
|
-
if (!syncTypes) {
|
|
57
|
-
throw new Error(lib.sandbox.sync.failure.syncTypeFetch);
|
|
58
|
-
}
|
|
59
|
-
return syncTypes.map(t => ({ type: t.name }));
|
|
60
|
-
}
|
|
61
40
|
export async function validateSandboxUsageLimits(accountConfig, sandboxType, env) {
|
|
62
41
|
const accountId = accountConfig.accountId;
|
|
63
42
|
if (!accountId) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
interface DevServerOptions {
|
|
3
|
+
absoluteSrc: string;
|
|
4
|
+
accountName?: string;
|
|
5
|
+
noSsl?: boolean;
|
|
6
|
+
port?: number;
|
|
7
|
+
generateFieldsTypes?: boolean;
|
|
8
|
+
resetSession?: boolean;
|
|
9
|
+
dest: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function spawnDevServer(options: DevServerOptions): Promise<ChildProcess>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { getConfigFilePath } from '@hubspot/local-dev-lib/config';
|
|
7
|
+
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
8
|
+
import { lib } from '../../lang/en.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
// cms-dev-server version to install to isolated cache
|
|
12
|
+
const TARGET_CMS_DEV_SERVER_VERSION = '1.2.16';
|
|
13
|
+
/**
|
|
14
|
+
* Ensures cms-dev-server is installed in an isolated cache directory.
|
|
15
|
+
* This prevents React version conflicts with the CLI.
|
|
16
|
+
*/
|
|
17
|
+
async function ensureCmsDevServerCache(targetVersion) {
|
|
18
|
+
const cacheDir = path.join(os.homedir(), '.hscli', '.module-cache');
|
|
19
|
+
const packageJsonPath = path.join(cacheDir, 'node_modules', '@hubspot', 'cms-dev-server', 'package.json');
|
|
20
|
+
// Check if already installed with correct version
|
|
21
|
+
let needsInstall = true;
|
|
22
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
23
|
+
try {
|
|
24
|
+
const installedPackage = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
25
|
+
if (installedPackage.version === targetVersion) {
|
|
26
|
+
needsInstall = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
// If we can't read the package.json, reinstall
|
|
31
|
+
needsInstall = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (needsInstall) {
|
|
35
|
+
// Show spinner during install (can take 10-30 seconds)
|
|
36
|
+
SpinniesManager.init({
|
|
37
|
+
succeedColor: 'white',
|
|
38
|
+
});
|
|
39
|
+
SpinniesManager.add('cms-dev-server-install', {
|
|
40
|
+
text: lib.theme.cmsDevServerProcess.installStarted(targetVersion),
|
|
41
|
+
});
|
|
42
|
+
// Create cache directory
|
|
43
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
44
|
+
// Clear old installation if exists
|
|
45
|
+
const nodeModulesDir = path.join(cacheDir, 'node_modules');
|
|
46
|
+
if (fs.existsSync(nodeModulesDir)) {
|
|
47
|
+
fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
// Install cms-dev-server with production dependencies only (async to allow spinner)
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
const installProcess = spawn('npm', [
|
|
52
|
+
'install',
|
|
53
|
+
`@hubspot/cms-dev-server@${targetVersion}`,
|
|
54
|
+
'--production',
|
|
55
|
+
'--no-save',
|
|
56
|
+
'--loglevel=error',
|
|
57
|
+
], {
|
|
58
|
+
cwd: cacheDir,
|
|
59
|
+
stdio: 'ignore', // Suppress npm output
|
|
60
|
+
});
|
|
61
|
+
installProcess.on('close', code => {
|
|
62
|
+
if (code === 0) {
|
|
63
|
+
SpinniesManager.succeed('cms-dev-server-install', {
|
|
64
|
+
text: lib.theme.cmsDevServerProcess.installSucceeded,
|
|
65
|
+
});
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
SpinniesManager.fail('cms-dev-server-install', {
|
|
70
|
+
text: lib.theme.cmsDevServerProcess.installFailed,
|
|
71
|
+
});
|
|
72
|
+
reject(new Error(lib.theme.cmsDevServerProcess.installFailed));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
installProcess.on('error', error => {
|
|
76
|
+
SpinniesManager.fail('cms-dev-server-install', {
|
|
77
|
+
text: lib.theme.cmsDevServerProcess.installFailed,
|
|
78
|
+
});
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return cacheDir;
|
|
84
|
+
}
|
|
85
|
+
export async function spawnDevServer(options) {
|
|
86
|
+
const { absoluteSrc, accountName, noSsl, port, generateFieldsTypes, resetSession, dest, } = options;
|
|
87
|
+
// Ensure cms-dev-server is installed in isolated cache
|
|
88
|
+
const cacheDir = await ensureCmsDevServerCache(TARGET_CMS_DEV_SERVER_VERSION);
|
|
89
|
+
// Get config path to pass to createDevServer
|
|
90
|
+
let configPath = '';
|
|
91
|
+
try {
|
|
92
|
+
configPath = process.env.HUBSPOT_CONFIG_PATH || getConfigFilePath();
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Config file doesn't exist - cms-dev-server will handle this gracefully
|
|
96
|
+
}
|
|
97
|
+
// Copy the runner script to the cache directory so imports resolve from there
|
|
98
|
+
// This is critical: Node resolves ES module imports relative to the script location,
|
|
99
|
+
// not the cwd. By copying the script to the cache directory, imports will resolve
|
|
100
|
+
// from the cache's node_modules (React 18) instead of the CLI's node_modules (React 19)
|
|
101
|
+
const sourceRunnerPath = path.join(__dirname, 'cmsDevServerRunner.js');
|
|
102
|
+
const targetRunnerPath = path.join(cacheDir, 'cmsPreviewRunner.js');
|
|
103
|
+
fs.copyFileSync(sourceRunnerPath, targetRunnerPath);
|
|
104
|
+
// Set environment variables to pass configuration to the runner script
|
|
105
|
+
const env = { ...process.env };
|
|
106
|
+
env.CMS_DEV_SERVER_SRC = absoluteSrc;
|
|
107
|
+
env.CMS_DEV_SERVER_DEST = dest;
|
|
108
|
+
env.CMS_DEV_SERVER_CONFIG = configPath;
|
|
109
|
+
env.CMS_DEV_SERVER_ACCOUNT = accountName || '';
|
|
110
|
+
env.CMS_DEV_SERVER_SSL = (!noSsl).toString();
|
|
111
|
+
env.CMS_DEV_SERVER_FIELD_GEN = Boolean(generateFieldsTypes).toString();
|
|
112
|
+
env.CMS_DEV_SERVER_RESET_SESSION = Boolean(resetSession).toString();
|
|
113
|
+
if (port) {
|
|
114
|
+
env.PORT = port.toString();
|
|
115
|
+
}
|
|
116
|
+
// Suppress Node.js deprecation warnings
|
|
117
|
+
env.NODE_NO_WARNINGS = '1';
|
|
118
|
+
// Spawn Node with the runner script from the isolated cache directory
|
|
119
|
+
// This ensures complete isolation from CLI's React 19
|
|
120
|
+
const devServer = spawn('node', [targetRunnerPath], {
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
env,
|
|
123
|
+
cwd: cacheDir,
|
|
124
|
+
});
|
|
125
|
+
// Handle process events
|
|
126
|
+
devServer.on('error', error => {
|
|
127
|
+
console.error(lib.theme.cmsDevServerProcess.serverStartError(error));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
devServer.on('exit', (code, signal) => {
|
|
131
|
+
if (code !== 0 && code !== null) {
|
|
132
|
+
console.error(lib.theme.cmsDevServerProcess.serverExit(code));
|
|
133
|
+
process.exit(code);
|
|
134
|
+
}
|
|
135
|
+
if (signal) {
|
|
136
|
+
console.error(lib.theme.cmsDevServerProcess.serverKill(signal));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Handle CLI termination
|
|
141
|
+
process.once('SIGINT', () => {
|
|
142
|
+
devServer.kill('SIGINT');
|
|
143
|
+
});
|
|
144
|
+
process.once('SIGTERM', () => {
|
|
145
|
+
devServer.kill('SIGTERM');
|
|
146
|
+
});
|
|
147
|
+
return devServer;
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This script runs in an isolated cache directory with cms-dev-server installed.
|
|
3
|
+
* It is spawned as a separate process to avoid React version conflicts with the CLI.
|
|
4
|
+
*
|
|
5
|
+
* Arguments are passed via environment variables:
|
|
6
|
+
* - CMS_DEV_SERVER_SRC: Source directory path
|
|
7
|
+
* - CMS_DEV_SERVER_DEST: Destination path
|
|
8
|
+
* - CMS_DEV_SERVER_CONFIG: Config file path (optional)
|
|
9
|
+
* - CMS_DEV_SERVER_ACCOUNT: Account name (optional)
|
|
10
|
+
* - CMS_DEV_SERVER_SSL: 'true' or 'false'
|
|
11
|
+
* - CMS_DEV_SERVER_FIELD_GEN: 'true' or 'false'
|
|
12
|
+
* - CMS_DEV_SERVER_RESET_SESSION: 'true' or 'false'
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* This script runs in an isolated cache directory with cms-dev-server installed.
|
|
4
|
+
* It is spawned as a separate process to avoid React version conflicts with the CLI.
|
|
5
|
+
*
|
|
6
|
+
* Arguments are passed via environment variables:
|
|
7
|
+
* - CMS_DEV_SERVER_SRC: Source directory path
|
|
8
|
+
* - CMS_DEV_SERVER_DEST: Destination path
|
|
9
|
+
* - CMS_DEV_SERVER_CONFIG: Config file path (optional)
|
|
10
|
+
* - CMS_DEV_SERVER_ACCOUNT: Account name (optional)
|
|
11
|
+
* - CMS_DEV_SERVER_SSL: 'true' or 'false'
|
|
12
|
+
* - CMS_DEV_SERVER_FIELD_GEN: 'true' or 'false'
|
|
13
|
+
* - CMS_DEV_SERVER_RESET_SESSION: 'true' or 'false'
|
|
14
|
+
*/
|
|
15
|
+
// Suppress library deprecation warnings (e.g., body-parser)
|
|
16
|
+
process.noDeprecation = true;
|
|
17
|
+
// Dynamic imports to use the isolated cms-dev-server installation
|
|
18
|
+
const { createDevServer } = await import('@hubspot/cms-dev-server');
|
|
19
|
+
const { walk } = await import('@hubspot/local-dev-lib/fs');
|
|
20
|
+
const { createIgnoreFilter } = await import('@hubspot/local-dev-lib/ignoreRules');
|
|
21
|
+
const { isAllowedExtension } = await import('@hubspot/local-dev-lib/path');
|
|
22
|
+
const { FILE_UPLOAD_RESULT_TYPES } = await import('@hubspot/local-dev-lib/constants/files');
|
|
23
|
+
const cliProgress = (await import('cli-progress')).default;
|
|
24
|
+
// Read configuration from environment variables
|
|
25
|
+
const src = process.env.CMS_DEV_SERVER_SRC;
|
|
26
|
+
const dest = process.env.CMS_DEV_SERVER_DEST;
|
|
27
|
+
const configPath = process.env.CMS_DEV_SERVER_CONFIG || '';
|
|
28
|
+
const accountName = process.env.CMS_DEV_SERVER_ACCOUNT || '';
|
|
29
|
+
const sslEnabled = process.env.CMS_DEV_SERVER_SSL === 'true';
|
|
30
|
+
const fieldGenEnabled = process.env.CMS_DEV_SERVER_FIELD_GEN === 'true';
|
|
31
|
+
const resetSession = process.env.CMS_DEV_SERVER_RESET_SESSION === 'true';
|
|
32
|
+
// Get uploadable files for preview
|
|
33
|
+
let filePaths = [];
|
|
34
|
+
try {
|
|
35
|
+
filePaths = await walk(src);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.error('Error walking directory:', e);
|
|
39
|
+
}
|
|
40
|
+
filePaths = filePaths
|
|
41
|
+
.filter(file => isAllowedExtension(file))
|
|
42
|
+
.filter(createIgnoreFilter(false));
|
|
43
|
+
// Create progress bar for initial upload
|
|
44
|
+
function startProgressBar(numFiles) {
|
|
45
|
+
const initialUploadProgressBar = new cliProgress.SingleBar({
|
|
46
|
+
gracefulExit: true,
|
|
47
|
+
format: '[{bar}] {percentage}% | {value}/{total} | {label}',
|
|
48
|
+
hideCursor: true,
|
|
49
|
+
}, cliProgress.Presets.rect);
|
|
50
|
+
initialUploadProgressBar.start(numFiles, 0, {
|
|
51
|
+
label: 'Preparing upload...',
|
|
52
|
+
});
|
|
53
|
+
let uploadsHaveStarted = false;
|
|
54
|
+
return {
|
|
55
|
+
onAttemptCallback: () => { },
|
|
56
|
+
onSuccessCallback: () => {
|
|
57
|
+
initialUploadProgressBar.increment();
|
|
58
|
+
if (!uploadsHaveStarted) {
|
|
59
|
+
uploadsHaveStarted = true;
|
|
60
|
+
initialUploadProgressBar.update(0, {
|
|
61
|
+
label: 'Uploading files...',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
onFirstErrorCallback: () => { },
|
|
66
|
+
onRetryCallback: () => { },
|
|
67
|
+
onFinalErrorCallback: () => initialUploadProgressBar.increment(),
|
|
68
|
+
// eslint-disable-next-line
|
|
69
|
+
onFinishCallback: (results) => {
|
|
70
|
+
initialUploadProgressBar.update(numFiles, {
|
|
71
|
+
label: 'Upload complete',
|
|
72
|
+
});
|
|
73
|
+
initialUploadProgressBar.stop();
|
|
74
|
+
results.forEach(result => {
|
|
75
|
+
if (result.resultType === FILE_UPLOAD_RESULT_TYPES.FAILURE) {
|
|
76
|
+
console.error(`Failed to upload ${result.file}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const themePreviewOptions = {
|
|
83
|
+
filePaths,
|
|
84
|
+
startProgressBar,
|
|
85
|
+
resetSession,
|
|
86
|
+
dest,
|
|
87
|
+
};
|
|
88
|
+
createDevServer(src, false, // storybook
|
|
89
|
+
configPath, accountName, sslEnabled, fieldGenEnabled, themePreviewOptions);
|
|
90
|
+
export {};
|
package/lib/usageTracking.js
CHANGED
|
@@ -44,11 +44,14 @@ export async function trackCommandUsage(command, meta = {}, accountId) {
|
|
|
44
44
|
uiLogger.debug(`Attempting to track usage of "${command}" command`);
|
|
45
45
|
let authType = 'unknown';
|
|
46
46
|
if (accountId) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
try {
|
|
48
|
+
const accountConfig = getConfigAccountById(accountId);
|
|
49
|
+
authType =
|
|
50
|
+
accountConfig && accountConfig.authType
|
|
51
|
+
? accountConfig.authType
|
|
52
|
+
: API_KEY_AUTH_METHOD.value;
|
|
53
|
+
}
|
|
54
|
+
catch (e) { }
|
|
52
55
|
}
|
|
53
56
|
return trackCliInteraction({
|
|
54
57
|
action: 'cli-command',
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/cli",
|
|
3
|
-
"version": "8.0.10-experimental.
|
|
3
|
+
"version": "8.0.10-experimental.3",
|
|
4
4
|
"description": "The official CLI for developing on HubSpot",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": "https://github.com/HubSpot/hubspot-cli",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@hubspot/cms-dev-server": "1.2.16",
|
|
10
9
|
"@hubspot/local-dev-lib": "5.1.2",
|
|
11
|
-
"@hubspot/project-parsing-lib": "0.
|
|
10
|
+
"@hubspot/project-parsing-lib": "0.13.0-beta.0",
|
|
12
11
|
"@hubspot/serverless-dev-runtime": "7.0.7",
|
|
13
12
|
"@hubspot/ui-extensions-dev-server": "2.0.0",
|
|
14
13
|
"@inquirer/prompts": "7.1.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { uiLogger } from '../ui/logger.js';
|
|
2
|
-
import { initiateSync } from '@hubspot/local-dev-lib/api/sandboxSync';
|
|
3
|
-
import { getConfigAccountIfExists, getConfigAccountById, } from '@hubspot/local-dev-lib/config';
|
|
4
|
-
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
|
|
5
|
-
import { mockHubSpotHttpError } from '../testUtils.js';
|
|
6
|
-
import { getAvailableSyncTypes } from '../sandboxes.js';
|
|
7
|
-
import { syncSandbox } from '../sandboxSync.js';
|
|
8
|
-
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
9
|
-
vi.mock('@hubspot/local-dev-lib/api/sandboxSync');
|
|
10
|
-
vi.mock('@hubspot/local-dev-lib/config');
|
|
11
|
-
vi.mock('../sandboxes');
|
|
12
|
-
vi.mock('../ui/SpinniesManager');
|
|
13
|
-
const mockedUiLogger = uiLogger;
|
|
14
|
-
const mockedInitiateSync = initiateSync;
|
|
15
|
-
const mockedGetConfigAccountIfExists = getConfigAccountIfExists;
|
|
16
|
-
const mockedGetConfigAccountById = getConfigAccountById;
|
|
17
|
-
const mockedGetAvailableSyncTypes = getAvailableSyncTypes;
|
|
18
|
-
const mockedSpinniesInit = SpinniesManager.init;
|
|
19
|
-
const mockedSpinniesAdd = SpinniesManager.add;
|
|
20
|
-
const mockedSpinniesSucceed = SpinniesManager.succeed;
|
|
21
|
-
const mockedSpinniesFail = SpinniesManager.fail;
|
|
22
|
-
describe('lib/sandboxSync', () => {
|
|
23
|
-
const mockEnv = 'qa';
|
|
24
|
-
const mockParentAccount = {
|
|
25
|
-
name: 'Parent Account',
|
|
26
|
-
accountId: 123,
|
|
27
|
-
accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX,
|
|
28
|
-
env: mockEnv,
|
|
29
|
-
authType: 'personalaccesskey',
|
|
30
|
-
};
|
|
31
|
-
const mockChildAccount = {
|
|
32
|
-
name: 'Child Account',
|
|
33
|
-
accountId: 456,
|
|
34
|
-
accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
|
|
35
|
-
env: mockEnv,
|
|
36
|
-
authType: 'personalaccesskey',
|
|
37
|
-
};
|
|
38
|
-
const mockChildAccountWithMissingId = {
|
|
39
|
-
name: 'Child Account',
|
|
40
|
-
accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
|
|
41
|
-
env: mockEnv,
|
|
42
|
-
authType: 'personalaccesskey',
|
|
43
|
-
};
|
|
44
|
-
const mockSyncTasks = [{ type: 'mock-sync-type' }];
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
mockedGetConfigAccountIfExists
|
|
47
|
-
.mockReturnValueOnce(mockChildAccount)
|
|
48
|
-
.mockReturnValueOnce(mockParentAccount);
|
|
49
|
-
mockedGetAvailableSyncTypes.mockResolvedValue(mockSyncTasks);
|
|
50
|
-
// Mock SpinniesManager methods to prevent spinner errors
|
|
51
|
-
mockedSpinniesInit.mockImplementation(() => { });
|
|
52
|
-
mockedSpinniesAdd.mockImplementation(() => { });
|
|
53
|
-
mockedSpinniesSucceed.mockImplementation(() => { });
|
|
54
|
-
mockedSpinniesFail.mockImplementation(() => { });
|
|
55
|
-
// Mock account config for uiAccountDescription calls
|
|
56
|
-
mockedGetConfigAccountById.mockImplementation(accountId => {
|
|
57
|
-
if (accountId === mockChildAccount.accountId) {
|
|
58
|
-
return mockChildAccount;
|
|
59
|
-
}
|
|
60
|
-
if (accountId === mockParentAccount.accountId) {
|
|
61
|
-
return mockParentAccount;
|
|
62
|
-
}
|
|
63
|
-
return undefined; // Don't throw, just return undefined for unknown accounts
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('syncSandbox()', () => {
|
|
67
|
-
it('successfully syncs a sandbox with provided sync tasks', async () => {
|
|
68
|
-
mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
|
|
69
|
-
await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks);
|
|
70
|
-
expect(mockedSpinniesInit).toHaveBeenCalled();
|
|
71
|
-
expect(mockedSpinniesAdd).toHaveBeenCalled();
|
|
72
|
-
expect(mockedInitiateSync).toHaveBeenCalledWith(mockParentAccount.accountId, mockChildAccount.accountId, mockSyncTasks, mockChildAccount.accountId);
|
|
73
|
-
expect(mockedSpinniesSucceed).toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
it('fetches sync types when no tasks are provided', async () => {
|
|
76
|
-
mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
|
|
77
|
-
await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, []);
|
|
78
|
-
expect(mockedGetAvailableSyncTypes).toHaveBeenCalledWith(mockParentAccount, mockChildAccount);
|
|
79
|
-
expect(mockedGetAvailableSyncTypes).toHaveBeenCalledWith(mockParentAccount, mockChildAccount);
|
|
80
|
-
expect(mockedInitiateSync).toHaveBeenCalled();
|
|
81
|
-
});
|
|
82
|
-
it('throws error when account IDs are missing', async () => {
|
|
83
|
-
const errorRegex = new RegExp(`because your account has been removed from`);
|
|
84
|
-
await expect(syncSandbox(mockChildAccountWithMissingId, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toThrow(errorRegex);
|
|
85
|
-
});
|
|
86
|
-
it('handles sync in progress error', async () => {
|
|
87
|
-
const error = mockHubSpotHttpError('', {
|
|
88
|
-
status: 429,
|
|
89
|
-
data: {
|
|
90
|
-
category: 'RATE_LIMITS',
|
|
91
|
-
subCategory: 'sandboxes-sync-api.SYNC_IN_PROGRESS',
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
mockedInitiateSync.mockRejectedValue(error);
|
|
95
|
-
await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
|
|
96
|
-
expect(mockedSpinniesFail).toHaveBeenCalled();
|
|
97
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Couldn't run the sync because there's another sync in progress/));
|
|
98
|
-
});
|
|
99
|
-
it('handles invalid user error', async () => {
|
|
100
|
-
const error = mockHubSpotHttpError('', {
|
|
101
|
-
status: 403,
|
|
102
|
-
data: {
|
|
103
|
-
category: 'BANNED',
|
|
104
|
-
subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USER',
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
mockedInitiateSync.mockRejectedValue(error);
|
|
108
|
-
await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
|
|
109
|
-
expect(mockedSpinniesFail).toHaveBeenCalled();
|
|
110
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/because your account has been removed from/));
|
|
111
|
-
});
|
|
112
|
-
it('handles not super admin error', async () => {
|
|
113
|
-
const error = mockHubSpotHttpError('', {
|
|
114
|
-
status: 403,
|
|
115
|
-
data: {
|
|
116
|
-
category: 'BANNED',
|
|
117
|
-
subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USERID',
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
mockedInitiateSync.mockRejectedValue(error);
|
|
121
|
-
await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
|
|
122
|
-
expect(mockedSpinniesFail).toHaveBeenCalled();
|
|
123
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Couldn't run the sync because you are not a super admin in/));
|
|
124
|
-
});
|
|
125
|
-
it('handles sandbox not found error', async () => {
|
|
126
|
-
const error = mockHubSpotHttpError('', {
|
|
127
|
-
status: 404,
|
|
128
|
-
data: {
|
|
129
|
-
category: 'OBJECT_NOT_FOUND',
|
|
130
|
-
subCategory: 'SandboxErrors.SANDBOX_NOT_FOUND',
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
mockedInitiateSync.mockRejectedValue(error);
|
|
134
|
-
await expect(syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks)).rejects.toEqual(error);
|
|
135
|
-
expect(mockedSpinniesFail).toHaveBeenCalled();
|
|
136
|
-
expect(mockedUiLogger.error).toHaveBeenCalledWith(expect.stringMatching(/may have been deleted through the UI/));
|
|
137
|
-
});
|
|
138
|
-
it('displays slim info message when specified', async () => {
|
|
139
|
-
mockedInitiateSync.mockResolvedValue({ status: 'SUCCESS' });
|
|
140
|
-
await syncSandbox(mockChildAccount, mockParentAccount, mockEnv, mockSyncTasks, true);
|
|
141
|
-
expect(mockedUiLogger.info).not.toHaveBeenCalled();
|
|
142
|
-
expect(mockedSpinniesSucceed).toHaveBeenCalledWith('sandboxSync', expect.objectContaining({
|
|
143
|
-
text: expect.stringMatching(/Initiated sync of object definitions from production to /),
|
|
144
|
-
}));
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
});
|
package/lib/sandboxSync.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
|
|
2
|
-
import { Environment } from '@hubspot/local-dev-lib/types/Accounts';
|
|
3
|
-
import { SandboxSyncTask } from '../types/Sandboxes.js';
|
|
4
|
-
export declare function syncSandbox(accountConfig: HubSpotConfigAccount, parentAccountConfig: HubSpotConfigAccount, env: Environment, syncTasks: Array<SandboxSyncTask>, slimInfoMessage?: boolean): Promise<void>;
|
package/lib/sandboxSync.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import SpinniesManager from './ui/SpinniesManager.js';
|
|
2
|
-
import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls';
|
|
3
|
-
import { uiLogger } from './ui/logger.js';
|
|
4
|
-
import { initiateSync } from '@hubspot/local-dev-lib/api/sandboxSync';
|
|
5
|
-
import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index';
|
|
6
|
-
import { lib } from '../lang/en.js';
|
|
7
|
-
import { getAvailableSyncTypes, getSandboxTypeAsString } from './sandboxes.js';
|
|
8
|
-
import { debugError, logError, ApiErrorContext, } from './errorHandlers/index.js';
|
|
9
|
-
import { uiAccountDescription, uiLine, uiCommandDisabledBanner, } from './ui/index.js';
|
|
10
|
-
import { isDevelopmentSandbox } from './accountTypes.js';
|
|
11
|
-
export async function syncSandbox(accountConfig, parentAccountConfig, env, syncTasks, slimInfoMessage = false) {
|
|
12
|
-
const accountId = accountConfig.accountId;
|
|
13
|
-
const parentAccountId = parentAccountConfig.accountId;
|
|
14
|
-
const isDevSandbox = isDevelopmentSandbox(accountConfig);
|
|
15
|
-
if (!accountId || !parentAccountId) {
|
|
16
|
-
throw new Error(lib.sandbox.sync.failure.invalidUser(uiAccountDescription(accountId), uiAccountDescription(parentAccountId)));
|
|
17
|
-
}
|
|
18
|
-
SpinniesManager.init({
|
|
19
|
-
succeedColor: 'white',
|
|
20
|
-
});
|
|
21
|
-
let availableSyncTasks = syncTasks;
|
|
22
|
-
const baseUrl = getHubSpotWebsiteOrigin(env);
|
|
23
|
-
const syncStatusUrl = `${baseUrl}/sandboxes-developer/${parentAccountId}/${getSandboxTypeAsString(accountConfig.accountType)}`;
|
|
24
|
-
try {
|
|
25
|
-
// If no sync tasks exist, fetch sync types based on default account. Parent account required for fetch
|
|
26
|
-
if (!availableSyncTasks ||
|
|
27
|
-
(typeof availableSyncTasks === 'object' &&
|
|
28
|
-
availableSyncTasks.length === 0)) {
|
|
29
|
-
availableSyncTasks = await getAvailableSyncTypes(parentAccountConfig, accountConfig);
|
|
30
|
-
}
|
|
31
|
-
SpinniesManager.add('sandboxSync', {
|
|
32
|
-
text: lib.sandbox.sync.loading.startSync,
|
|
33
|
-
});
|
|
34
|
-
await initiateSync(parentAccountId, accountId, availableSyncTasks, accountId);
|
|
35
|
-
const spinniesText = isDevSandbox
|
|
36
|
-
? lib.sandbox.sync.loading.succeedDevSb(accountId)
|
|
37
|
-
: lib.sandbox.sync.loading.succeed(accountId);
|
|
38
|
-
SpinniesManager.succeed('sandboxSync', {
|
|
39
|
-
text: slimInfoMessage
|
|
40
|
-
? lib.sandbox.sync.loading.successDevSbInfo(accountId, syncStatusUrl)
|
|
41
|
-
: spinniesText,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
debugError(err);
|
|
46
|
-
SpinniesManager.fail('sandboxSync', {
|
|
47
|
-
text: lib.sandbox.sync.loading.fail(accountId),
|
|
48
|
-
});
|
|
49
|
-
uiLogger.log('');
|
|
50
|
-
if (isSpecifiedError(err, {
|
|
51
|
-
statusCode: 403,
|
|
52
|
-
category: 'BANNED',
|
|
53
|
-
subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USER',
|
|
54
|
-
})) {
|
|
55
|
-
uiLogger.error(lib.sandbox.sync.failure.invalidUser(uiAccountDescription(accountId), uiAccountDescription(parentAccountId)));
|
|
56
|
-
}
|
|
57
|
-
else if (isSpecifiedError(err, {
|
|
58
|
-
statusCode: 429,
|
|
59
|
-
category: 'RATE_LIMITS',
|
|
60
|
-
subCategory: 'sandboxes-sync-api.SYNC_IN_PROGRESS',
|
|
61
|
-
})) {
|
|
62
|
-
uiLogger.error(lib.sandbox.sync.failure.syncInProgress(`${baseUrl}/sandboxes-developer/${parentAccountId}/syncactivitylog`));
|
|
63
|
-
}
|
|
64
|
-
else if (isSpecifiedError(err, {
|
|
65
|
-
statusCode: 403,
|
|
66
|
-
category: 'BANNED',
|
|
67
|
-
subCategory: 'sandboxes-sync-api.SYNC_NOT_ALLOWED_INVALID_USERID',
|
|
68
|
-
})) {
|
|
69
|
-
// This will only trigger if a user is not a super admin of the target account.
|
|
70
|
-
uiLogger.error(lib.sandbox.sync.failure.notSuperAdmin(accountId));
|
|
71
|
-
}
|
|
72
|
-
else if (isSpecifiedError(err, {
|
|
73
|
-
statusCode: 404,
|
|
74
|
-
category: 'OBJECT_NOT_FOUND',
|
|
75
|
-
subCategory: 'SandboxErrors.SANDBOX_NOT_FOUND',
|
|
76
|
-
})) {
|
|
77
|
-
uiLogger.error(lib.sandbox.sync.failure.objectNotFound(accountId));
|
|
78
|
-
}
|
|
79
|
-
else if (isSpecifiedError(err, {
|
|
80
|
-
statusCode: 404,
|
|
81
|
-
})) {
|
|
82
|
-
uiCommandDisabledBanner('hs sandbox sync', 'https://developers.hubspot.com/docs/developer-tooling/local-development/hubspot-cli/project-commands');
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
logError(err, new ApiErrorContext({
|
|
86
|
-
accountId: parentAccountId,
|
|
87
|
-
request: 'sandbox sync',
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
uiLogger.log('');
|
|
91
|
-
throw err;
|
|
92
|
-
}
|
|
93
|
-
if (!slimInfoMessage) {
|
|
94
|
-
uiLogger.log('');
|
|
95
|
-
uiLine();
|
|
96
|
-
uiLogger.info(isDevSandbox
|
|
97
|
-
? lib.sandbox.sync.info.syncMessageDevSb(syncStatusUrl)
|
|
98
|
-
: lib.sandbox.sync.info.syncMessage(syncStatusUrl));
|
|
99
|
-
uiLine();
|
|
100
|
-
uiLogger.log('');
|
|
101
|
-
}
|
|
102
|
-
}
|