@hubspot/cli 8.0.2-experimental.0 → 8.0.3-experimental.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/__tests__/getStarted.test.js +2 -2
- package/commands/__tests__/project.test.js +30 -0
- package/commands/account/auth.js +8 -97
- package/commands/account/use.js +19 -4
- package/commands/cms/module/marketplace-validate.js +23 -5
- package/commands/cms/theme/marketplace-validate.js +25 -6
- package/commands/mcp/setup.js +1 -2
- package/commands/mcp.js +1 -2
- package/commands/project.js +22 -1
- package/lang/en.d.ts +30 -1
- package/lang/en.js +34 -5
- package/lib/__tests__/accountAuth.test.d.ts +1 -0
- package/lib/__tests__/accountAuth.test.js +258 -0
- package/lib/accountAuth.d.ts +10 -0
- package/lib/accountAuth.js +105 -0
- package/lib/app/urls.d.ts +1 -0
- package/lib/app/urls.js +4 -0
- package/lib/errors/ProjectErrors.d.ts +15 -0
- package/lib/errors/ProjectErrors.js +30 -0
- package/lib/getStarted/getStartedV2.js +3 -41
- package/lib/getStartedV2Actions.d.ts +29 -0
- package/lib/getStartedV2Actions.js +104 -9
- package/lib/marketplaceValidate.d.ts +1 -1
- package/lib/marketplaceValidate.js +23 -41
- package/lib/projects/ProjectLogsManager.d.ts +12 -3
- package/lib/projects/ProjectLogsManager.js +70 -12
- package/lib/projects/__tests__/ProjectLogsManager.test.js +131 -18
- package/lib/projects/__tests__/platformVersion.test.js +37 -1
- package/lib/projects/__tests__/projects.test.js +6 -2
- package/lib/projects/__tests__/upload.test.js +10 -0
- package/lib/projects/__tests__/workspaceArchive.test.d.ts +1 -0
- package/lib/projects/__tests__/workspaceArchive.test.js +207 -0
- package/lib/projects/components.d.ts +6 -0
- package/lib/projects/components.js +1 -1
- package/lib/projects/config.js +9 -2
- package/lib/projects/localDev/helpers/project.d.ts +4 -1
- package/lib/projects/localDev/helpers/project.js +13 -8
- package/lib/projects/platformVersion.d.ts +8 -0
- package/lib/projects/platformVersion.js +31 -2
- package/lib/projects/upload.js +9 -0
- package/lib/projects/workspaces.d.ts +36 -0
- package/lib/projects/workspaces.js +224 -0
- package/lib/prompts/accountsPrompt.d.ts +2 -1
- package/lib/prompts/accountsPrompt.js +10 -2
- package/package.json +3 -3
- package/ui/components/ActionSection.d.ts +1 -1
- package/ui/components/BoxWithTitle.js +1 -1
- package/ui/components/getStarted/GetStartedFlow.d.ts +8 -0
- package/ui/components/getStarted/GetStartedFlow.js +136 -0
- package/ui/components/getStarted/reducer.d.ts +59 -0
- package/ui/components/getStarted/reducer.js +72 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.d.ts +16 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +39 -0
- package/ui/components/getStarted/screens/UploadScreen.d.ts +7 -0
- package/ui/components/getStarted/screens/UploadScreen.js +43 -0
- package/ui/components/getStarted/selectors.d.ts +2 -0
- package/ui/components/getStarted/selectors.js +1 -0
- package/ui/lib/constants.d.ts +16 -0
- package/ui/lib/constants.js +16 -0
- package/ui/render.js +6 -0
- package/ui/components/GetStartedFlow.d.ts +0 -24
- package/ui/components/GetStartedFlow.js +0 -128
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { updateConfigAccount, createEmptyConfigFile, getConfigFilePath, localConfigFileExists, globalConfigFileExists, setConfigAccountAsDefault, } from '@hubspot/local-dev-lib/config';
|
|
2
|
+
import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
3
|
+
import { toKebabCase } from '@hubspot/local-dev-lib/text';
|
|
4
|
+
import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth';
|
|
5
|
+
import { handleMerge, handleMigration } from '../configMigrate.js';
|
|
6
|
+
import { personalAccessKeyPrompt } from '../prompts/personalAccessKeyPrompt.js';
|
|
7
|
+
import { cliAccountNamePrompt } from '../prompts/accountNamePrompt.js';
|
|
8
|
+
import { setAsDefaultAccountPrompt } from '../prompts/setAsDefaultAccountPrompt.js';
|
|
9
|
+
import { authenticateNewAccount } from '../accountAuth.js';
|
|
10
|
+
vi.mock('@hubspot/local-dev-lib/config');
|
|
11
|
+
vi.mock('@hubspot/local-dev-lib/personalAccessKey');
|
|
12
|
+
vi.mock('@hubspot/local-dev-lib/text');
|
|
13
|
+
vi.mock('../configMigrate.js');
|
|
14
|
+
vi.mock('../errorHandlers/index.js');
|
|
15
|
+
vi.mock('../prompts/personalAccessKeyPrompt.js');
|
|
16
|
+
vi.mock('../prompts/accountNamePrompt.js');
|
|
17
|
+
vi.mock('../prompts/setAsDefaultAccountPrompt.js');
|
|
18
|
+
vi.mock('../ui/logger.js', () => ({
|
|
19
|
+
uiLogger: {
|
|
20
|
+
log: vi.fn(),
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
success: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
const mockedUpdateConfigAccount = updateConfigAccount;
|
|
26
|
+
const mockedCreateEmptyConfigFile = createEmptyConfigFile;
|
|
27
|
+
const mockedGetConfigFilePath = getConfigFilePath;
|
|
28
|
+
const mockedLocalConfigFileExists = localConfigFileExists;
|
|
29
|
+
const mockedGlobalConfigFileExists = globalConfigFileExists;
|
|
30
|
+
const mockedSetConfigAccountAsDefault = setConfigAccountAsDefault;
|
|
31
|
+
const mockedGetAccessToken = getAccessToken;
|
|
32
|
+
const mockedUpdateConfigWithAccessToken = updateConfigWithAccessToken;
|
|
33
|
+
const mockedToKebabCase = toKebabCase;
|
|
34
|
+
const mockedHandleMerge = handleMerge;
|
|
35
|
+
const mockedHandleMigration = handleMigration;
|
|
36
|
+
const mockedPersonalAccessKeyPrompt = personalAccessKeyPrompt;
|
|
37
|
+
const mockedCliAccountNamePrompt = cliAccountNamePrompt;
|
|
38
|
+
const mockedSetAsDefaultAccountPrompt = setAsDefaultAccountPrompt;
|
|
39
|
+
describe('lib/accountAuth', () => {
|
|
40
|
+
describe('authenticateNewAccount()', () => {
|
|
41
|
+
const mockAccessToken = {
|
|
42
|
+
portalId: 123456,
|
|
43
|
+
accessToken: 'test-token',
|
|
44
|
+
expiresAt: '2025-01-01',
|
|
45
|
+
scopeGroups: ['test-scope'],
|
|
46
|
+
enabledFeatures: { 'test-feature': 1 },
|
|
47
|
+
encodedOAuthRefreshToken: 'test-refresh-token',
|
|
48
|
+
hubName: 'Test Hub',
|
|
49
|
+
accountType: 'STANDARD',
|
|
50
|
+
};
|
|
51
|
+
const mockAccountConfig = {
|
|
52
|
+
name: 'test-hub',
|
|
53
|
+
accountId: 123456,
|
|
54
|
+
env: 'prod',
|
|
55
|
+
accountType: 'STANDARD',
|
|
56
|
+
authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value,
|
|
57
|
+
personalAccessKey: 'test-key',
|
|
58
|
+
auth: {
|
|
59
|
+
tokenInfo: {
|
|
60
|
+
accessToken: 'test-token',
|
|
61
|
+
expiresAt: '2025-01-01',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
mockedLocalConfigFileExists.mockReturnValue(false);
|
|
68
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
69
|
+
mockedGetAccessToken.mockResolvedValue(mockAccessToken);
|
|
70
|
+
mockedUpdateConfigWithAccessToken.mockResolvedValue(mockAccountConfig);
|
|
71
|
+
mockedToKebabCase.mockReturnValue('test-hub');
|
|
72
|
+
mockedCliAccountNamePrompt.mockResolvedValue({ name: 'test-hub' });
|
|
73
|
+
mockedGetConfigFilePath.mockReturnValue('/path/to/config');
|
|
74
|
+
mockedPersonalAccessKeyPrompt.mockResolvedValue({
|
|
75
|
+
personalAccessKey: 'test-key',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('should create config file if it does not exist', async () => {
|
|
79
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
80
|
+
await authenticateNewAccount({
|
|
81
|
+
env: 'prod',
|
|
82
|
+
providedPersonalAccessKey: 'test-key',
|
|
83
|
+
accountId: 123456,
|
|
84
|
+
});
|
|
85
|
+
expect(mockedCreateEmptyConfigFile).toHaveBeenCalledWith(true);
|
|
86
|
+
});
|
|
87
|
+
it('should not create config file if it already exists', async () => {
|
|
88
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
89
|
+
await authenticateNewAccount({
|
|
90
|
+
env: 'prod',
|
|
91
|
+
providedPersonalAccessKey: 'test-key',
|
|
92
|
+
accountId: 123456,
|
|
93
|
+
});
|
|
94
|
+
expect(mockedCreateEmptyConfigFile).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
it('should use provided personal access key without prompting', async () => {
|
|
97
|
+
await authenticateNewAccount({
|
|
98
|
+
env: 'prod',
|
|
99
|
+
providedPersonalAccessKey: 'test-key',
|
|
100
|
+
accountId: 123456,
|
|
101
|
+
});
|
|
102
|
+
expect(mockedPersonalAccessKeyPrompt).not.toHaveBeenCalled();
|
|
103
|
+
expect(mockedGetAccessToken).toHaveBeenCalledWith('test-key', 'prod');
|
|
104
|
+
});
|
|
105
|
+
it('should prompt for personal access key if not provided', async () => {
|
|
106
|
+
await authenticateNewAccount({
|
|
107
|
+
env: 'prod',
|
|
108
|
+
accountId: 123456,
|
|
109
|
+
});
|
|
110
|
+
expect(mockedPersonalAccessKeyPrompt).toHaveBeenCalledWith({
|
|
111
|
+
env: 'prod',
|
|
112
|
+
account: 123456,
|
|
113
|
+
});
|
|
114
|
+
expect(mockedGetAccessToken).toHaveBeenCalledWith('test-key', 'prod');
|
|
115
|
+
});
|
|
116
|
+
it('should prompt for account name if config does not exist', async () => {
|
|
117
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
118
|
+
await authenticateNewAccount({
|
|
119
|
+
env: 'prod',
|
|
120
|
+
providedPersonalAccessKey: 'test-key',
|
|
121
|
+
accountId: 123456,
|
|
122
|
+
});
|
|
123
|
+
expect(mockedCliAccountNamePrompt).toHaveBeenCalledWith('test-hub');
|
|
124
|
+
});
|
|
125
|
+
it('should not prompt for account name if config already exists', async () => {
|
|
126
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
127
|
+
await authenticateNewAccount({
|
|
128
|
+
env: 'prod',
|
|
129
|
+
providedPersonalAccessKey: 'test-key',
|
|
130
|
+
accountId: 123456,
|
|
131
|
+
});
|
|
132
|
+
expect(mockedCliAccountNamePrompt).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
it('should set account as default when setAsDefaultAccount is true', async () => {
|
|
135
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
136
|
+
await authenticateNewAccount({
|
|
137
|
+
env: 'prod',
|
|
138
|
+
providedPersonalAccessKey: 'test-key',
|
|
139
|
+
accountId: 123456,
|
|
140
|
+
setAsDefaultAccount: true,
|
|
141
|
+
});
|
|
142
|
+
expect(mockedSetConfigAccountAsDefault).toHaveBeenCalledWith('test-hub');
|
|
143
|
+
});
|
|
144
|
+
it('should prompt to set as default when setAsDefaultAccount is not provided and config exists', async () => {
|
|
145
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
146
|
+
await authenticateNewAccount({
|
|
147
|
+
env: 'prod',
|
|
148
|
+
providedPersonalAccessKey: 'test-key',
|
|
149
|
+
accountId: 123456,
|
|
150
|
+
});
|
|
151
|
+
expect(mockedSetAsDefaultAccountPrompt).toHaveBeenCalledWith('test-hub');
|
|
152
|
+
});
|
|
153
|
+
it('should return the updated account config on success', async () => {
|
|
154
|
+
const result = await authenticateNewAccount({
|
|
155
|
+
env: 'prod',
|
|
156
|
+
providedPersonalAccessKey: 'test-key',
|
|
157
|
+
accountId: 123456,
|
|
158
|
+
});
|
|
159
|
+
expect(result).toEqual(mockAccountConfig);
|
|
160
|
+
});
|
|
161
|
+
it('should return null if access token fetch fails', async () => {
|
|
162
|
+
mockedGetAccessToken.mockRejectedValue(new Error('Invalid token'));
|
|
163
|
+
const result = await authenticateNewAccount({
|
|
164
|
+
env: 'prod',
|
|
165
|
+
providedPersonalAccessKey: 'test-key',
|
|
166
|
+
accountId: 123456,
|
|
167
|
+
});
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
it('should return null if config update fails', async () => {
|
|
171
|
+
mockedUpdateConfigWithAccessToken.mockResolvedValue(null);
|
|
172
|
+
const result = await authenticateNewAccount({
|
|
173
|
+
env: 'prod',
|
|
174
|
+
providedPersonalAccessKey: 'test-key',
|
|
175
|
+
accountId: 123456,
|
|
176
|
+
});
|
|
177
|
+
expect(result).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
it('should handle missing account name and prompt for it', async () => {
|
|
180
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
181
|
+
mockedUpdateConfigWithAccessToken.mockResolvedValue({
|
|
182
|
+
...mockAccountConfig,
|
|
183
|
+
name: undefined,
|
|
184
|
+
});
|
|
185
|
+
mockedCliAccountNamePrompt.mockResolvedValue({
|
|
186
|
+
name: 'new-account-name',
|
|
187
|
+
});
|
|
188
|
+
await authenticateNewAccount({
|
|
189
|
+
env: 'prod',
|
|
190
|
+
providedPersonalAccessKey: 'test-key',
|
|
191
|
+
accountId: 123456,
|
|
192
|
+
});
|
|
193
|
+
expect(mockedCliAccountNamePrompt).toHaveBeenCalledWith('test-hub');
|
|
194
|
+
expect(mockedUpdateConfigAccount).toHaveBeenCalledWith(expect.objectContaining({
|
|
195
|
+
name: 'new-account-name',
|
|
196
|
+
}));
|
|
197
|
+
});
|
|
198
|
+
describe('config migration', () => {
|
|
199
|
+
it('should handle migration when local config exists and global does not', async () => {
|
|
200
|
+
mockedLocalConfigFileExists.mockReturnValue(true);
|
|
201
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
202
|
+
mockedHandleMigration.mockResolvedValue(true);
|
|
203
|
+
await authenticateNewAccount({
|
|
204
|
+
env: 'prod',
|
|
205
|
+
providedPersonalAccessKey: 'test-key',
|
|
206
|
+
accountId: 123456,
|
|
207
|
+
});
|
|
208
|
+
expect(mockedHandleMigration).toHaveBeenCalled();
|
|
209
|
+
expect(mockedHandleMerge).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
it('should handle merge when both local and global configs exist', async () => {
|
|
212
|
+
mockedLocalConfigFileExists.mockReturnValue(true);
|
|
213
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
214
|
+
mockedHandleMerge.mockResolvedValue(true);
|
|
215
|
+
await authenticateNewAccount({
|
|
216
|
+
env: 'prod',
|
|
217
|
+
providedPersonalAccessKey: 'test-key',
|
|
218
|
+
accountId: 123456,
|
|
219
|
+
});
|
|
220
|
+
expect(mockedHandleMerge).toHaveBeenCalled();
|
|
221
|
+
expect(mockedHandleMigration).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
it('should return null if migration is not confirmed', async () => {
|
|
224
|
+
mockedLocalConfigFileExists.mockReturnValue(true);
|
|
225
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
226
|
+
mockedHandleMigration.mockResolvedValue(false);
|
|
227
|
+
const result = await authenticateNewAccount({
|
|
228
|
+
env: 'prod',
|
|
229
|
+
providedPersonalAccessKey: 'test-key',
|
|
230
|
+
accountId: 123456,
|
|
231
|
+
});
|
|
232
|
+
expect(result).toBeNull();
|
|
233
|
+
});
|
|
234
|
+
it('should return null if merge is not confirmed', async () => {
|
|
235
|
+
mockedLocalConfigFileExists.mockReturnValue(true);
|
|
236
|
+
mockedGlobalConfigFileExists.mockReturnValue(true);
|
|
237
|
+
mockedHandleMerge.mockResolvedValue(false);
|
|
238
|
+
const result = await authenticateNewAccount({
|
|
239
|
+
env: 'prod',
|
|
240
|
+
providedPersonalAccessKey: 'test-key',
|
|
241
|
+
accountId: 123456,
|
|
242
|
+
});
|
|
243
|
+
expect(result).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
it('should return null if migration throws error', async () => {
|
|
246
|
+
mockedLocalConfigFileExists.mockReturnValue(true);
|
|
247
|
+
mockedGlobalConfigFileExists.mockReturnValue(false);
|
|
248
|
+
mockedHandleMigration.mockRejectedValue(new Error('Migration failed'));
|
|
249
|
+
const result = await authenticateNewAccount({
|
|
250
|
+
env: 'prod',
|
|
251
|
+
providedPersonalAccessKey: 'test-key',
|
|
252
|
+
accountId: 123456,
|
|
253
|
+
});
|
|
254
|
+
expect(result).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Environment } from '@hubspot/local-dev-lib/types/Accounts';
|
|
2
|
+
import { HubSpotConfigAccount } from '@hubspot/local-dev-lib/types/Accounts';
|
|
3
|
+
type AuthenticateNewAccountOptions = {
|
|
4
|
+
env: Environment;
|
|
5
|
+
providedPersonalAccessKey?: string;
|
|
6
|
+
accountId?: number;
|
|
7
|
+
setAsDefaultAccount?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, }: AuthenticateNewAccountOptions): Promise<HubSpotConfigAccount | null>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { updateConfigAccount, createEmptyConfigFile, getConfigFilePath, localConfigFileExists, globalConfigFileExists, setConfigAccountAsDefault, } from '@hubspot/local-dev-lib/config';
|
|
2
|
+
import { getAccessToken, updateConfigWithAccessToken, } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
3
|
+
import { toKebabCase } from '@hubspot/local-dev-lib/text';
|
|
4
|
+
import { handleMerge, handleMigration } from './configMigrate.js';
|
|
5
|
+
import { debugError, logError } from './errorHandlers/index.js';
|
|
6
|
+
import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt.js';
|
|
7
|
+
import { cliAccountNamePrompt } from './prompts/accountNamePrompt.js';
|
|
8
|
+
import { setAsDefaultAccountPrompt } from './prompts/setAsDefaultAccountPrompt.js';
|
|
9
|
+
import { commands } from '../lang/en.js';
|
|
10
|
+
import { uiLogger } from './ui/logger.js';
|
|
11
|
+
async function updateConfigWithNewAccount(env, configAlreadyExists, providedPersonalAccessKey, accountId) {
|
|
12
|
+
try {
|
|
13
|
+
const { personalAccessKey } = providedPersonalAccessKey
|
|
14
|
+
? { personalAccessKey: providedPersonalAccessKey }
|
|
15
|
+
: await personalAccessKeyPrompt({
|
|
16
|
+
env,
|
|
17
|
+
account: accountId,
|
|
18
|
+
});
|
|
19
|
+
const token = await getAccessToken(personalAccessKey, env);
|
|
20
|
+
const defaultAccountName = token.hubName
|
|
21
|
+
? toKebabCase(token.hubName)
|
|
22
|
+
: undefined;
|
|
23
|
+
const accountName = configAlreadyExists
|
|
24
|
+
? undefined
|
|
25
|
+
: (await cliAccountNamePrompt(defaultAccountName)).name;
|
|
26
|
+
const updatedConfig = await updateConfigWithAccessToken(token, personalAccessKey, env, accountName, !configAlreadyExists);
|
|
27
|
+
if (!updatedConfig)
|
|
28
|
+
return null;
|
|
29
|
+
// Can happen if the user is re-authenticating an account with no name
|
|
30
|
+
if (configAlreadyExists && !updatedConfig.name) {
|
|
31
|
+
updatedConfig.name = (await cliAccountNamePrompt(defaultAccountName)).name;
|
|
32
|
+
updateConfigAccount({
|
|
33
|
+
...updatedConfig,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return updatedConfig;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
debugError(e);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function handleConfigMigration() {
|
|
44
|
+
const deprecatedConfigExists = localConfigFileExists();
|
|
45
|
+
const globalConfigExists = globalConfigFileExists();
|
|
46
|
+
if (!deprecatedConfigExists) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (globalConfigExists) {
|
|
50
|
+
try {
|
|
51
|
+
const mergeConfirmed = await handleMerge();
|
|
52
|
+
if (!mergeConfirmed) {
|
|
53
|
+
uiLogger.log('');
|
|
54
|
+
uiLogger.log(commands.account.subcommands.auth.errors.mergeNotConfirmed);
|
|
55
|
+
}
|
|
56
|
+
return mergeConfirmed;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logError(error);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const migrationConfirmed = await handleMigration();
|
|
65
|
+
if (!migrationConfirmed) {
|
|
66
|
+
uiLogger.log('');
|
|
67
|
+
uiLogger.log(commands.account.subcommands.auth.errors.migrationNotConfirmed);
|
|
68
|
+
}
|
|
69
|
+
return migrationConfirmed;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logError(error);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function authenticateNewAccount({ env, providedPersonalAccessKey, accountId, setAsDefaultAccount, }) {
|
|
77
|
+
const configMigrationSuccess = await handleConfigMigration();
|
|
78
|
+
if (!configMigrationSuccess) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const configAlreadyExists = globalConfigFileExists();
|
|
82
|
+
if (!configAlreadyExists) {
|
|
83
|
+
createEmptyConfigFile(true);
|
|
84
|
+
}
|
|
85
|
+
const updatedConfig = await updateConfigWithNewAccount(env, configAlreadyExists, providedPersonalAccessKey, accountId);
|
|
86
|
+
if (!updatedConfig) {
|
|
87
|
+
uiLogger.error(commands.account.subcommands.auth.errors.failedToUpdateConfig);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const { accountId: newAccountId, name } = updatedConfig;
|
|
91
|
+
if (!configAlreadyExists) {
|
|
92
|
+
uiLogger.log('');
|
|
93
|
+
uiLogger.success(commands.account.subcommands.auth.success.configFileCreated(getConfigFilePath()));
|
|
94
|
+
uiLogger.success(commands.account.subcommands.auth.success.configFileUpdated(newAccountId));
|
|
95
|
+
}
|
|
96
|
+
else if (setAsDefaultAccount) {
|
|
97
|
+
setConfigAccountAsDefault(name);
|
|
98
|
+
uiLogger.log('');
|
|
99
|
+
uiLogger.success(commands.account.subcommands.auth.success.configFileUpdated(newAccountId));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await setAsDefaultAccountPrompt(name);
|
|
103
|
+
}
|
|
104
|
+
return updatedConfig;
|
|
105
|
+
}
|
package/lib/app/urls.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ type PublicAppInstallUrlArgs = {
|
|
|
13
13
|
};
|
|
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
|
+
export declare function getAppCardSetupUrl({ targetAccountId, env, appId, }: PrivateAppInstallUrlArgs): string;
|
|
16
17
|
export {};
|
package/lib/app/urls.js
CHANGED
|
@@ -10,3 +10,7 @@ export function getStaticAuthAppInstallUrl({ targetAccountId, env, appId, }) {
|
|
|
10
10
|
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
11
11
|
return `${websiteOrigin}/static-token/${targetAccountId}/authorize?appId=${appId}`;
|
|
12
12
|
}
|
|
13
|
+
export function getAppCardSetupUrl({ targetAccountId, env, appId, }) {
|
|
14
|
+
const websiteOrigin = getHubSpotWebsiteOrigin(env);
|
|
15
|
+
return `${websiteOrigin}/integrations-settings/${targetAccountId}/installed/framework/${appId}/app-cards?tourId=get-started`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class ProjectNestingError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class ProjectConfigNotFoundError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class ProjectValidationError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class ProjectUploadError extends Error {
|
|
11
|
+
constructor(message: string, cause?: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare class ProjectBuildDeployError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class ProjectNestingError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ProjectNestingError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ProjectConfigNotFoundError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ProjectConfigNotFoundError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ProjectValidationError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'ProjectValidationError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ProjectUploadError extends Error {
|
|
20
|
+
constructor(message, cause) {
|
|
21
|
+
super(message, { cause });
|
|
22
|
+
this.name = 'ProjectUploadError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ProjectBuildDeployError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ProjectBuildDeployError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,54 +1,16 @@
|
|
|
1
1
|
import { LOG_LEVEL, setLogLevel } from '@hubspot/local-dev-lib/logger';
|
|
2
|
-
import {
|
|
3
|
-
import { trackCommandMetadataUsage } from '../usageTracking.js';
|
|
2
|
+
import { trackGetStartedUsage } from '../../lib/getStartedV2Actions.js';
|
|
4
3
|
import { renderInteractive } from '../../ui/render.js';
|
|
5
|
-
import { getGetStartedFlow } from '../../ui/components/GetStartedFlow.js';
|
|
6
|
-
import { getErrorMessage } from '../errorHandlers/index.js';
|
|
4
|
+
import { getGetStartedFlow } from '../../ui/components/getStarted/GetStartedFlow.js';
|
|
7
5
|
export async function runGetStartedV2({ derivedAccountId, name, dest, }) {
|
|
8
6
|
setLogLevel(LOG_LEVEL.NONE);
|
|
9
|
-
const onRunCreateProject = async (args) => {
|
|
10
|
-
try {
|
|
11
|
-
const result = await createProjectAction(args);
|
|
12
|
-
await trackCommandMetadataUsage('get-started', {
|
|
13
|
-
successful: true,
|
|
14
|
-
step: 'github-clone',
|
|
15
|
-
}, derivedAccountId);
|
|
16
|
-
await trackCommandMetadataUsage('get-started', {
|
|
17
|
-
successful: true,
|
|
18
|
-
step: 'project-creation',
|
|
19
|
-
}, derivedAccountId);
|
|
20
|
-
return {
|
|
21
|
-
projectName: result.projectName,
|
|
22
|
-
projectDest: result.projectDest,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
const errorMessage = getErrorMessage(error);
|
|
27
|
-
// Check if this is a nested project error (happens before cloning)
|
|
28
|
-
const isNestedProjectError = errorMessage.includes('Projects cannot be nested within other projects');
|
|
29
|
-
if (isNestedProjectError) {
|
|
30
|
-
await trackCommandMetadataUsage('get-started', {
|
|
31
|
-
successful: false,
|
|
32
|
-
step: 'project-creation',
|
|
33
|
-
}, derivedAccountId);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
// Clone or other error (failedToDownloadProject message)
|
|
37
|
-
await trackCommandMetadataUsage('get-started', {
|
|
38
|
-
successful: false,
|
|
39
|
-
step: 'github-clone',
|
|
40
|
-
}, derivedAccountId);
|
|
41
|
-
}
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
7
|
try {
|
|
46
8
|
await renderInteractive(getGetStartedFlow({
|
|
47
9
|
derivedAccountId,
|
|
48
|
-
onRunCreateProject,
|
|
49
10
|
initialName: name,
|
|
50
11
|
initialDest: dest,
|
|
51
12
|
}), { fullScreen: true });
|
|
13
|
+
await trackGetStartedUsage({ successful: true, step: 'command-completed' }, derivedAccountId);
|
|
52
14
|
}
|
|
53
15
|
finally {
|
|
54
16
|
setLogLevel(LOG_LEVEL.LOG);
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
import { type ProjectMetadata } from '@hubspot/project-parsing-lib/projects';
|
|
2
|
+
import { type IntermediateRepresentationNode } from '@hubspot/project-parsing-lib/translate';
|
|
1
3
|
export type CreateProjectResult = {
|
|
2
4
|
projectName: string;
|
|
3
5
|
projectDest: string;
|
|
4
6
|
};
|
|
7
|
+
export type AppConfig = {
|
|
8
|
+
name?: string;
|
|
9
|
+
uid?: string;
|
|
10
|
+
distribution?: string;
|
|
11
|
+
auth?: {
|
|
12
|
+
type?: string;
|
|
13
|
+
requiredScopes?: string[];
|
|
14
|
+
optionalScopes?: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type AppIRNode = IntermediateRepresentationNode & {
|
|
18
|
+
config: AppConfig;
|
|
19
|
+
};
|
|
5
20
|
export declare function createProjectAction({ projectName, projectDest, }: {
|
|
6
21
|
projectName: string;
|
|
7
22
|
projectDest: string;
|
|
8
23
|
}): Promise<CreateProjectResult>;
|
|
24
|
+
export type UploadAndDeployResult = {
|
|
25
|
+
appId: number;
|
|
26
|
+
projectId: number;
|
|
27
|
+
installUrl: string;
|
|
28
|
+
projectDir: string;
|
|
29
|
+
app: AppIRNode;
|
|
30
|
+
projectName: string;
|
|
31
|
+
projectMetadata: ProjectMetadata;
|
|
32
|
+
};
|
|
33
|
+
export declare function uploadAndDeployAction({ accountId, projectDest, }: {
|
|
34
|
+
accountId: number;
|
|
35
|
+
projectDest: string;
|
|
36
|
+
}): Promise<UploadAndDeployResult>;
|
|
37
|
+
export declare function trackGetStartedUsage(params: Record<string, unknown>, accountId: number): Promise<void>;
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPublicAppsForPortal } from '@hubspot/local-dev-lib/api/appsDev';
|
|
4
|
+
import { fetchProject } from '@hubspot/local-dev-lib/api/projects';
|
|
5
|
+
import { getConfigAccountEnvironment } from '@hubspot/local-dev-lib/config';
|
|
4
6
|
import { cloneGithubRepo } from '@hubspot/local-dev-lib/github';
|
|
7
|
+
import { getCwd } from '@hubspot/local-dev-lib/path';
|
|
8
|
+
import { getProjectMetadata, } from '@hubspot/project-parsing-lib/projects';
|
|
9
|
+
import { translate, } from '@hubspot/project-parsing-lib/translate';
|
|
5
10
|
import { commands, lib } from '../lang/en.js';
|
|
11
|
+
import { getStaticAuthAppInstallUrl } from '../lib/app/urls.js';
|
|
6
12
|
import { HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, PROJECT_CONFIG_FILE, } from '../lib/constants.js';
|
|
7
|
-
import {
|
|
13
|
+
import { ProjectNestingError, ProjectConfigNotFoundError, ProjectValidationError, ProjectUploadError, ProjectBuildDeployError, } from './errors/ProjectErrors.js';
|
|
14
|
+
import { getProjectConfig, validateProjectConfig, writeProjectConfig, } from '../lib/projects/config.js';
|
|
15
|
+
import { isV2Project } from '../lib/projects/platformVersion.js';
|
|
16
|
+
import { pollProjectBuildAndDeploy } from '../lib/projects/pollProjectBuildAndDeploy.js';
|
|
17
|
+
import { handleProjectUpload } from '../lib/projects/upload.js';
|
|
8
18
|
import { validateProjectDirectory } from '../lib/prompts/projectNameAndDestPrompt.js';
|
|
9
|
-
import
|
|
19
|
+
import { trackCommandMetadataUsage } from './usageTracking.js';
|
|
10
20
|
export async function createProjectAction({ projectName, projectDest, }) {
|
|
11
|
-
// Validate project name
|
|
12
21
|
if (!projectName || projectName.trim() === '') {
|
|
13
22
|
throw new Error(lib.prompts.projectNameAndDestPrompt.errors.nameRequired);
|
|
14
23
|
}
|
|
15
|
-
// Validate destination path
|
|
16
24
|
const validationResult = validateProjectDirectory(projectDest);
|
|
17
25
|
if (validationResult !== true) {
|
|
18
26
|
throw new Error(typeof validationResult === 'string'
|
|
@@ -24,9 +32,8 @@ export async function createProjectAction({ projectName, projectDest, }) {
|
|
|
24
32
|
if (existingProjectConfig &&
|
|
25
33
|
existingProjectDir &&
|
|
26
34
|
projectDestAbsolute.startsWith(existingProjectDir)) {
|
|
27
|
-
throw new
|
|
35
|
+
throw new ProjectNestingError(commands.project.create.errors.cannotNestProjects(existingProjectDir));
|
|
28
36
|
}
|
|
29
|
-
SpinniesManager.setDisableOutput(true);
|
|
30
37
|
try {
|
|
31
38
|
await cloneGithubRepo(HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH, projectDestAbsolute, {
|
|
32
39
|
sourceDir: '2025.2/private-app-get-started-template',
|
|
@@ -45,7 +52,95 @@ export async function createProjectAction({ projectName, projectDest, }) {
|
|
|
45
52
|
cause: error,
|
|
46
53
|
});
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
}
|
|
56
|
+
async function fetchAppAfterDeploy(accountId, env) {
|
|
57
|
+
const { data: { results }, } = await fetchPublicAppsForPortal(accountId);
|
|
58
|
+
const lastCreatedApp = results.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
59
|
+
if (!lastCreatedApp) {
|
|
60
|
+
throw new Error(commands.getStarted.errors.noAppsFound);
|
|
61
|
+
}
|
|
62
|
+
const installUrl = getStaticAuthAppInstallUrl({
|
|
63
|
+
targetAccountId: accountId,
|
|
64
|
+
env,
|
|
65
|
+
appId: lastCreatedApp.id,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
appId: lastCreatedApp.id,
|
|
69
|
+
installUrl,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function buildProjectMetadata(projectDir, projectConfig, accountId) {
|
|
73
|
+
const srcDir = path.join(projectDir, projectConfig.srcDir);
|
|
74
|
+
const projectMetadata = await getProjectMetadata(srcDir);
|
|
75
|
+
const intermediateRepresentation = await translate({
|
|
76
|
+
projectSourceDir: srcDir,
|
|
77
|
+
platformVersion: projectConfig.platformVersion,
|
|
78
|
+
accountId,
|
|
79
|
+
}, { skipValidation: false });
|
|
80
|
+
const apps = Object.values(intermediateRepresentation.intermediateNodesIndexedByUid).filter(node => node.componentType === 'APPLICATION');
|
|
81
|
+
if (apps.length === 0) {
|
|
82
|
+
throw new Error(commands.getStarted.errors.noAppsFound);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
projectMetadata,
|
|
86
|
+
app: apps[0],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function uploadAndDeployAction({ accountId, projectDest, }) {
|
|
90
|
+
const { projectConfig, projectDir } = await getProjectConfig(projectDest);
|
|
91
|
+
if (!projectConfig || !projectDir) {
|
|
92
|
+
throw new ProjectConfigNotFoundError(commands.getStarted.errors.configFileNotFound);
|
|
50
93
|
}
|
|
94
|
+
try {
|
|
95
|
+
validateProjectConfig(projectConfig, projectDir);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new ProjectValidationError(message);
|
|
100
|
+
}
|
|
101
|
+
const env = getConfigAccountEnvironment(accountId);
|
|
102
|
+
try {
|
|
103
|
+
const { result, uploadError } = await handleProjectUpload({
|
|
104
|
+
accountId,
|
|
105
|
+
projectConfig,
|
|
106
|
+
projectDir,
|
|
107
|
+
callbackFunc: pollProjectBuildAndDeploy,
|
|
108
|
+
uploadMessage: commands.getStarted.logs.initialUploadMessage,
|
|
109
|
+
forceCreate: true,
|
|
110
|
+
isUploadCommand: false,
|
|
111
|
+
sendIR: isV2Project(projectConfig.platformVersion),
|
|
112
|
+
skipValidation: false,
|
|
113
|
+
});
|
|
114
|
+
if (uploadError) {
|
|
115
|
+
throw new ProjectUploadError(commands.getStarted.errors.uploadActionFailed, uploadError);
|
|
116
|
+
}
|
|
117
|
+
if (!result || !result.succeeded) {
|
|
118
|
+
throw new ProjectBuildDeployError(commands.getStarted.errors.buildOrDeployFailed);
|
|
119
|
+
}
|
|
120
|
+
const { data: projectData } = await fetchProject(accountId, projectConfig.name);
|
|
121
|
+
const projectId = projectData.id;
|
|
122
|
+
const { appId, installUrl } = await fetchAppAfterDeploy(accountId, env);
|
|
123
|
+
const { projectMetadata, app } = await buildProjectMetadata(projectDir, projectConfig, accountId);
|
|
124
|
+
return {
|
|
125
|
+
appId,
|
|
126
|
+
projectId,
|
|
127
|
+
installUrl,
|
|
128
|
+
projectDir,
|
|
129
|
+
app,
|
|
130
|
+
projectName: projectConfig.name,
|
|
131
|
+
projectMetadata,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof ProjectUploadError ||
|
|
136
|
+
error instanceof ProjectBuildDeployError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(commands.getStarted.errors.failedToUploadAndDeploy, {
|
|
140
|
+
cause: error,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function trackGetStartedUsage(params, accountId) {
|
|
145
|
+
return trackCommandMetadataUsage('get-started', params, accountId);
|
|
51
146
|
}
|