@hubspot/cli 8.0.0 → 8.0.2-experimental.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bin/cli.js +8 -5
  2. package/commands/__tests__/getStarted.test.js +12 -0
  3. package/commands/getStarted.d.ts +2 -1
  4. package/commands/getStarted.js +38 -15
  5. package/lang/en.d.ts +31 -5
  6. package/lang/en.js +33 -7
  7. package/lib/CLIWebSocketServer.d.ts +28 -0
  8. package/lib/CLIWebSocketServer.js +91 -0
  9. package/lib/__tests__/CLIWebSocketServer.test.d.ts +1 -0
  10. package/lib/__tests__/CLIWebSocketServer.test.js +252 -0
  11. package/lib/__tests__/commandSuggestion.test.d.ts +1 -0
  12. package/lib/__tests__/commandSuggestion.test.js +119 -0
  13. package/lib/commandSuggestion.d.ts +3 -0
  14. package/lib/commandSuggestion.js +45 -0
  15. package/lib/constants.d.ts +0 -1
  16. package/lib/constants.js +0 -1
  17. package/lib/getStarted/getStartedV2.d.ts +7 -0
  18. package/lib/getStarted/getStartedV2.js +56 -0
  19. package/lib/getStartedV2Actions.d.ts +8 -0
  20. package/lib/getStartedV2Actions.js +51 -0
  21. package/lib/mcp/setup.js +48 -54
  22. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +43 -175
  23. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -7
  24. package/lib/projects/localDev/LocalDevWebsocketServer.js +51 -98
  25. package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +8 -7
  26. package/lib/projects/platformVersion.js +1 -1
  27. package/package.json +9 -4
  28. package/types/LocalDev.d.ts +0 -4
  29. package/ui/components/ActionSection.d.ts +12 -0
  30. package/ui/components/ActionSection.js +25 -0
  31. package/ui/components/BoxWithTitle.d.ts +4 -2
  32. package/ui/components/BoxWithTitle.js +2 -2
  33. package/ui/components/FullScreen.d.ts +6 -0
  34. package/ui/components/FullScreen.js +13 -0
  35. package/ui/components/GetStartedFlow.d.ts +24 -0
  36. package/ui/components/GetStartedFlow.js +128 -0
  37. package/ui/components/InputField.d.ts +10 -0
  38. package/ui/components/InputField.js +10 -0
  39. package/ui/components/SelectInput.d.ts +11 -0
  40. package/ui/components/SelectInput.js +59 -0
  41. package/ui/components/StatusIcon.d.ts +9 -0
  42. package/ui/components/StatusIcon.js +17 -0
  43. package/ui/constants.d.ts +6 -0
  44. package/ui/constants.js +6 -0
  45. package/ui/playground/fixtures.js +47 -0
  46. package/ui/render.d.ts +4 -0
  47. package/ui/render.js +25 -0
  48. package/ui/styles.d.ts +3 -0
  49. package/ui/styles.js +3 -0
@@ -1,41 +1,24 @@
1
- import { WebSocketServer } from 'ws';
2
- import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
3
- import { uiLogger } from '../../ui/logger.js';
4
1
  import { addLocalStateFlag } from '@hubspot/local-dev-lib/config';
5
2
  import { LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, CONFIG_LOCAL_STATE_FLAGS, LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID, } from '../../constants.js';
6
- import { lib } from '../../../lang/en.js';
7
3
  import { removeAnsiCodes } from '../../ui/removeAnsiCodes.js';
8
4
  import { isDeployWebsocketMessage, isViewedWelcomeScreenWebsocketMessage, isUploadWebsocketMessage, isAppInstallFailureWebsocketMessage, isAppInstallSuccessWebsocketMessage, isAppInstallInitiatedWebsocketMessage, } from './localDevWebsocketServerUtils.js';
9
- import { pkg } from '../../jsonLoader.js';
5
+ import CLIWebSocketServer from '../../CLIWebSocketServer.js';
10
6
  const LOCAL_DEV_WEBSOCKET_SERVER_VERSION = 2;
11
7
  const LOG_PREFIX = '[LocalDevWebsocketServer]';
12
- const DOMAINS = ['hubspot.com', 'hubspotqa.com'];
13
- const SUBDOMAINS = ['local', 'app', 'app-na2', 'app-na3', 'app-ap1', 'app-eu1'];
14
- const ALLOWED_ORIGIN_REGEX = new RegExp(`^https://(${SUBDOMAINS.join('|')})\\.(${DOMAINS.join('|')})$`);
15
8
  class LocalDevWebsocketServer {
16
- server;
17
- debug;
9
+ cliWebSocketServer;
18
10
  localDevProcess;
19
11
  constructor(localDevProcess, debug) {
20
12
  this.localDevProcess = localDevProcess;
21
- this.debug = debug;
22
- }
23
- log(message) {
24
- if (this.debug) {
25
- uiLogger.log(`${LOG_PREFIX} ${message}`);
26
- }
27
- }
28
- logError(message) {
29
- if (this.debug) {
30
- uiLogger.error(`${LOG_PREFIX} ${message}`);
31
- }
32
- }
33
- sendMessage(websocket, message) {
34
- websocket.send(JSON.stringify(message));
13
+ this.cliWebSocketServer = new CLIWebSocketServer({
14
+ instanceId: LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID,
15
+ logPrefix: LOG_PREFIX,
16
+ debug,
17
+ });
35
18
  }
36
19
  async handleUpload(websocket) {
37
20
  const { uploadSuccess, buildSuccess, deploySuccess, deployId } = await this.localDevProcess.uploadProject();
38
- this.sendMessage(websocket, {
21
+ this.cliWebSocketServer.sendMessage(websocket, {
39
22
  type: uploadSuccess
40
23
  ? LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPLOAD_SUCCESS
41
24
  : LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPLOAD_FAILURE,
@@ -50,7 +33,7 @@ class LocalDevWebsocketServer {
50
33
  }
51
34
  async handleDeploy(websocket, force) {
52
35
  const { success, deployId } = await this.localDevProcess.deployLatestBuild(force);
53
- this.sendMessage(websocket, {
36
+ this.cliWebSocketServer.sendMessage(websocket, {
54
37
  type: success
55
38
  ? LOCAL_DEV_UI_MESSAGE_SEND_TYPES.DEPLOY_SUCCESS
56
39
  : LOCAL_DEV_UI_MESSAGE_SEND_TYPES.DEPLOY_FAILURE,
@@ -70,52 +53,35 @@ class LocalDevWebsocketServer {
70
53
  async handleAppInstallInitiated() {
71
54
  this.localDevProcess.sendDevServerMessage(LOCAL_DEV_SERVER_MESSAGE_TYPES.OAUTH_APP_INSTALL_INITIATED);
72
55
  }
73
- setupMessageHandlers(websocket) {
74
- websocket.on('message', data => {
75
- try {
76
- const message = JSON.parse(data.toString());
77
- if (!message.type) {
78
- this.logError(lib.LocalDevWebsocketServer.errors.missingTypeField(data.toString()));
79
- return;
80
- }
81
- if (isUploadWebsocketMessage(message)) {
82
- this.handleUpload(websocket);
83
- }
84
- else if (isDeployWebsocketMessage(message)) {
85
- this.handleDeploy(websocket, message.data.force);
86
- }
87
- else if (isViewedWelcomeScreenWebsocketMessage(message)) {
88
- addLocalStateFlag(CONFIG_LOCAL_STATE_FLAGS.LOCAL_DEV_UI_WELCOME);
89
- }
90
- else if (isAppInstallSuccessWebsocketMessage(message)) {
91
- this.handleAppInstallSuccess();
92
- }
93
- else if (isAppInstallFailureWebsocketMessage(message)) {
94
- this.handleAppInstallFailure();
95
- }
96
- else if (isAppInstallInitiatedWebsocketMessage(message)) {
97
- this.handleAppInstallInitiated();
98
- }
99
- else {
100
- this.logError(lib.LocalDevWebsocketServer.errors.unknownMessageType(message.type));
101
- }
102
- }
103
- catch (e) {
104
- this.logError(lib.LocalDevWebsocketServer.errors.invalidJSON(data.toString()));
105
- }
106
- });
107
- }
108
- sendCliMetadata(websocket) {
109
- this.sendMessage(websocket, {
110
- type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.CLI_METADATA,
111
- data: {
112
- cliVersion: pkg.version,
113
- localDevWebsocketServerVersion: LOCAL_DEV_WEBSOCKET_SERVER_VERSION,
114
- },
115
- });
56
+ handleMessage(websocket, message) {
57
+ if (isUploadWebsocketMessage(message)) {
58
+ this.handleUpload(websocket);
59
+ return true;
60
+ }
61
+ else if (isDeployWebsocketMessage(message)) {
62
+ this.handleDeploy(websocket, message.data.force);
63
+ return true;
64
+ }
65
+ else if (isViewedWelcomeScreenWebsocketMessage(message)) {
66
+ addLocalStateFlag(CONFIG_LOCAL_STATE_FLAGS.LOCAL_DEV_UI_WELCOME);
67
+ return true;
68
+ }
69
+ else if (isAppInstallSuccessWebsocketMessage(message)) {
70
+ this.handleAppInstallSuccess();
71
+ return true;
72
+ }
73
+ else if (isAppInstallFailureWebsocketMessage(message)) {
74
+ this.handleAppInstallFailure();
75
+ return true;
76
+ }
77
+ else if (isAppInstallInitiatedWebsocketMessage(message)) {
78
+ this.handleAppInstallInitiated();
79
+ return true;
80
+ }
81
+ return false;
116
82
  }
117
83
  sendProjectData(websocket) {
118
- this.sendMessage(websocket, {
84
+ this.cliWebSocketServer.sendMessage(websocket, {
119
85
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
120
86
  data: {
121
87
  projectName: this.localDevProcess.projectData.name,
@@ -129,7 +95,7 @@ class LocalDevWebsocketServer {
129
95
  }
130
96
  setupProjectNodesListener(websocket) {
131
97
  const listener = (nodes) => {
132
- this.sendMessage(websocket, {
98
+ this.cliWebSocketServer.sendMessage(websocket, {
133
99
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
134
100
  data: nodes,
135
101
  });
@@ -141,7 +107,7 @@ class LocalDevWebsocketServer {
141
107
  }
142
108
  setupAppDataListener(websocket) {
143
109
  const listener = (appData) => {
144
- this.sendMessage(websocket, {
110
+ this.cliWebSocketServer.sendMessage(websocket, {
145
111
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_APP_DATA,
146
112
  data: appData,
147
113
  });
@@ -154,7 +120,7 @@ class LocalDevWebsocketServer {
154
120
  setupUploadWarningsListener(websocket) {
155
121
  const listener = (uploadWarnings) => {
156
122
  const formattedUploadWarnings = Array.from(uploadWarnings).map(removeAnsiCodes);
157
- this.sendMessage(websocket, {
123
+ this.cliWebSocketServer.sendMessage(websocket, {
158
124
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_UPLOAD_WARNINGS,
159
125
  data: { uploadWarnings: formattedUploadWarnings },
160
126
  });
@@ -167,7 +133,7 @@ class LocalDevWebsocketServer {
167
133
  setupDevServersStartedListener(websocket) {
168
134
  const listener = (devServersStarted) => {
169
135
  if (devServersStarted) {
170
- this.sendMessage(websocket, {
136
+ this.cliWebSocketServer.sendMessage(websocket, {
171
137
  type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.DEV_SERVERS_STARTED,
172
138
  });
173
139
  }
@@ -184,33 +150,20 @@ class LocalDevWebsocketServer {
184
150
  this.setupDevServersStartedListener(websocket);
185
151
  }
186
152
  async start() {
187
- const portManagerIsRunning = await isPortManagerServerRunning();
188
- if (!portManagerIsRunning) {
189
- throw new Error(lib.LocalDevWebsocketServer.errors.portManagerNotRunning(LOG_PREFIX));
190
- }
191
- const portData = await requestPorts([
192
- { instanceId: LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID },
193
- ]);
194
- const port = portData[LOCAL_DEV_WEBSOCKET_SERVER_INSTANCE_ID];
195
- this.server = new WebSocketServer({ port });
196
- this.log(lib.LocalDevWebsocketServer.logs.startup(port));
197
- this.server.on('connection', (ws, req) => {
198
- const origin = req.headers.origin;
199
- if (!origin || !ALLOWED_ORIGIN_REGEX.test(origin)) {
200
- ws.close(1008, lib.LocalDevWebsocketServer.errors.originNotAllowed(origin));
201
- return;
202
- }
203
- this.sendCliMetadata(ws);
204
- this.sendProjectData(ws);
205
- this.setupMessageHandlers(ws);
206
- this.setupStateListeners(ws);
207
- this.localDevProcess.sendDevServerMessage(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
153
+ return this.cliWebSocketServer.start({
154
+ metadata: {
155
+ localDevWebsocketServerVersion: LOCAL_DEV_WEBSOCKET_SERVER_VERSION,
156
+ },
157
+ onConnection: ws => {
158
+ this.sendProjectData(ws);
159
+ this.setupStateListeners(ws);
160
+ this.localDevProcess.sendDevServerMessage(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
161
+ },
162
+ onMessage: (ws, message) => this.handleMessage(ws, message),
208
163
  });
209
- this.server.on('close', () => { });
210
164
  }
211
165
  shutdown() {
212
- this.server?.close();
213
- this.server = undefined;
166
+ this.cliWebSocketServer.shutdown();
214
167
  }
215
168
  }
216
169
  export default LocalDevWebsocketServer;
@@ -1,7 +1,8 @@
1
- import { LocalDevDeployWebsocketMessage, LocalDevWebsocketMessage } from '../../../types/LocalDev.js';
2
- export declare function isUploadWebsocketMessage(message: LocalDevWebsocketMessage): boolean;
3
- export declare function isDeployWebsocketMessage(message: LocalDevWebsocketMessage): message is LocalDevDeployWebsocketMessage;
4
- export declare function isViewedWelcomeScreenWebsocketMessage(message: LocalDevWebsocketMessage): boolean;
5
- export declare function isAppInstallSuccessWebsocketMessage(message: LocalDevWebsocketMessage): boolean;
6
- export declare function isAppInstallInitiatedWebsocketMessage(message: LocalDevWebsocketMessage): boolean;
7
- export declare function isAppInstallFailureWebsocketMessage(message: LocalDevWebsocketMessage): boolean;
1
+ import { LocalDevDeployWebsocketMessage } from '../../../types/LocalDev.js';
2
+ import { CLIWebSocketMessage } from '../../CLIWebSocketServer.js';
3
+ export declare function isUploadWebsocketMessage(message: CLIWebSocketMessage): boolean;
4
+ export declare function isDeployWebsocketMessage(message: CLIWebSocketMessage): message is LocalDevDeployWebsocketMessage;
5
+ export declare function isViewedWelcomeScreenWebsocketMessage(message: CLIWebSocketMessage): boolean;
6
+ export declare function isAppInstallSuccessWebsocketMessage(message: CLIWebSocketMessage): boolean;
7
+ export declare function isAppInstallInitiatedWebsocketMessage(message: CLIWebSocketMessage): boolean;
8
+ export declare function isAppInstallFailureWebsocketMessage(message: CLIWebSocketMessage): boolean;
@@ -6,5 +6,5 @@ export function isV2Project(platformVersion) {
6
6
  return true;
7
7
  }
8
8
  const [year, minor] = platformVersion.split(/[.-]/);
9
- return Number(year) >= 2025 && Number(minor) >= 2;
9
+ return (Number(year) === 2025 && Number(minor) >= 2) || Number(year) > 2025;
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cli",
3
- "version": "8.0.0",
3
+ "version": "8.0.2-experimental.0",
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",
@@ -8,9 +8,9 @@
8
8
  "dependencies": {
9
9
  "@hubspot/cms-dev-server": "1.2.1",
10
10
  "@hubspot/local-dev-lib": "5.1.1",
11
- "@hubspot/project-parsing-lib": "0.11.2",
11
+ "@hubspot/project-parsing-lib": "0.12.0",
12
12
  "@hubspot/serverless-dev-runtime": "7.0.7",
13
- "@hubspot/ui-extensions-dev-server": "1.1.3",
13
+ "@hubspot/ui-extensions-dev-server": "1.1.8",
14
14
  "@inquirer/prompts": "7.1.0",
15
15
  "@modelcontextprotocol/sdk": "1.25.0",
16
16
  "archiver": "7.0.1",
@@ -22,6 +22,8 @@
22
22
  "findup-sync": "4.0.0",
23
23
  "fs-extra": "8.1.0",
24
24
  "ink": "6.6.0",
25
+ "ink-spinner": "5.0.0",
26
+ "ink-text-input": "6.0.0",
25
27
  "js-yaml": "4.1.0",
26
28
  "minimatch": "10.0.1",
27
29
  "moment": "2.30.1",
@@ -80,7 +82,7 @@
80
82
  "list-all-commands": "yarn tsx ./scripts/get-all-commands.ts",
81
83
  "local-link": "hubspot-linking",
82
84
  "mcp-local": "yarn tsx ./scripts/mcp-local.ts",
83
- "prettier:write": "prettier --write './**/*.{ts,js,json}'",
85
+ "prettier:write": "prettier --write './**/*.{ts,js,json,tsx}'",
84
86
  "release": "yarn tsx ./scripts/release.ts release",
85
87
  "sync-to-public": "yarn tsx ./scripts/sync-to-public.ts repo-sync",
86
88
  "ink-playground": "yarn build && yarn tsx ./scripts/ink-playground.ts",
@@ -115,5 +117,8 @@
115
117
  "publishConfig": {
116
118
  "access": "public",
117
119
  "registry": "https://registry.npmjs.org/"
120
+ },
121
+ "resolutions": {
122
+ "eslint-visitor-keys": "4.2.0"
118
123
  }
119
124
  }
@@ -20,10 +20,6 @@ export type LocalDevStateConstructorOptions = {
20
20
  initialProjectProfileData: HSProfileVariables;
21
21
  env: Environment;
22
22
  };
23
- export type LocalDevWebsocketMessage = {
24
- type: string;
25
- data?: unknown;
26
- };
27
23
  export type LocalDevDeployWebsocketMessage = {
28
24
  type: typeof LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.DEPLOY;
29
25
  data: {
@@ -0,0 +1,12 @@
1
+ import { ValueOf } from '@hubspot/local-dev-lib/types/Utils';
2
+ import { ACTION_STATUSES } from '../constants.js';
3
+ type ActionStatus = ValueOf<typeof ACTION_STATUSES>;
4
+ export type ActionSectionProps = {
5
+ status: ActionStatus;
6
+ statusText: string;
7
+ errorMessage?: string;
8
+ children: React.ReactNode;
9
+ };
10
+ export declare function ActionSection({ status, statusText, errorMessage, children, }: ActionSectionProps): import("react/jsx-runtime").JSX.Element | null;
11
+ export declare function getActionSection(props: ActionSectionProps): React.ReactNode;
12
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { ACTION_STATUSES } from '../constants.js';
4
+ import { INK_COLORS } from '../styles.js';
5
+ import { StatusIcon } from './StatusIcon.js';
6
+ const LEFT_BORDER_BOX_PROPS = {
7
+ flexDirection: 'column',
8
+ borderStyle: 'single',
9
+ borderColor: INK_COLORS.GRAY,
10
+ borderLeft: true,
11
+ borderTop: false,
12
+ borderRight: false,
13
+ borderBottom: false,
14
+ paddingLeft: 2,
15
+ marginLeft: 1,
16
+ };
17
+ export function ActionSection({ status, statusText, errorMessage, children, }) {
18
+ if (status === ACTION_STATUSES.IDLE) {
19
+ return null;
20
+ }
21
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", columnGap: 1, children: [_jsx(StatusIcon, { status: status }), _jsx(Text, { children: statusText })] }), _jsxs(Box, { ...LEFT_BORDER_BOX_PROPS, children: [children, status === ACTION_STATUSES.ERROR && errorMessage && (_jsx(Text, { color: INK_COLORS.ALERT_RED, wrap: "wrap", children: errorMessage }))] })] }));
22
+ }
23
+ export function getActionSection(props) {
24
+ return _jsx(ActionSection, { ...props });
25
+ }
@@ -1,9 +1,11 @@
1
1
  export interface BoxWithTitleProps {
2
2
  title: string;
3
- message: string;
3
+ message?: string;
4
4
  titleBackgroundColor?: string;
5
5
  borderColor?: string;
6
6
  textCentered?: boolean;
7
+ children?: React.ReactNode;
8
+ flexGrow?: number;
7
9
  }
8
10
  export declare function getBoxWithTitle(props: BoxWithTitleProps): React.ReactNode;
9
- export declare function BoxWithTitle({ title, message, titleBackgroundColor, borderColor, textCentered, }: BoxWithTitleProps): React.ReactNode;
11
+ export declare function BoxWithTitle({ title, message, titleBackgroundColor, borderColor, textCentered, children, flexGrow, }: BoxWithTitleProps): React.ReactNode;
@@ -4,6 +4,6 @@ import { CONTAINER_STYLES } from '../styles.js';
4
4
  export function getBoxWithTitle(props) {
5
5
  return _jsx(BoxWithTitle, { ...props });
6
6
  }
7
- export function BoxWithTitle({ title, message, titleBackgroundColor, borderColor, textCentered, }) {
8
- return (_jsxs(Box, { ...CONTAINER_STYLES, borderStyle: "round", borderColor: borderColor, alignSelf: "flex-start", children: [_jsx(Box, { position: "absolute", marginTop: -2, paddingX: 0, alignSelf: "flex-start", justifyContent: "center", alignItems: "center", children: _jsx(Text, { backgroundColor: titleBackgroundColor, bold: true, children: ` ${title} ` }) }), _jsx(Box, { flexDirection: "column", rowGap: 1, children: message?.split('\n\n').map((section, sectionIndex) => (_jsx(Box, { flexDirection: "column", alignItems: textCentered ? 'center' : 'flex-start', children: section.split('\n').map((line, lineIndex) => (_jsx(Text, { children: line }, `${sectionIndex}-${lineIndex}`))) }, sectionIndex))) })] }));
7
+ export function BoxWithTitle({ title, message, titleBackgroundColor, borderColor, textCentered, children, flexGrow, }) {
8
+ return (_jsxs(Box, { ...CONTAINER_STYLES, flexGrow: flexGrow, borderStyle: "round", borderColor: borderColor, children: [_jsx(Box, { position: "absolute", marginTop: -2, paddingX: 0, alignSelf: "flex-start", justifyContent: "center", alignItems: "center", children: _jsx(Text, { backgroundColor: titleBackgroundColor, bold: true, children: ` ${title} ` }) }), _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [message?.split('\n\n').map((section, sectionIndex) => (_jsx(Box, { flexDirection: "column", alignItems: textCentered ? 'center' : 'flex-start', children: section.split('\n').map((line, lineIndex) => (_jsx(Text, { children: line }, `${sectionIndex}-${lineIndex}`))) }, sectionIndex))), children] })] }));
9
9
  }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export type FullScreenProps = {
3
+ children: React.ReactNode;
4
+ };
5
+ export declare function FullScreen({ children }: FullScreenProps): React.ReactNode;
6
+ export declare function getFullScreen(props: FullScreenProps): React.ReactNode;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ import { useTerminalSize } from '../lib/useTerminalSize.js';
4
+ export function FullScreen({ children }) {
5
+ // Use rows - 1 to prevent flickering
6
+ // See: https://github.com/vadimdemedes/ink/issues/359
7
+ const { rows, columns } = useTerminalSize();
8
+ const height = rows - 1;
9
+ return (_jsx(Box, { flexDirection: "column", width: columns, height: height, overflow: "hidden", children: children }));
10
+ }
11
+ export function getFullScreen(props) {
12
+ return _jsx(FullScreen, { ...props });
13
+ }
@@ -0,0 +1,24 @@
1
+ import { SelectInputItem } from '../components/SelectInput.js';
2
+ export declare const DEFAULT_PROJECT_NAME = "get-started-project";
3
+ export declare const GET_STARTED_FLOW_OPTIONS: SelectInputItem[];
4
+ export declare const FLOW_STEPS: {
5
+ readonly SELECT: "select";
6
+ readonly NAME_INPUT: "name-input";
7
+ readonly DEST_INPUT: "dest-input";
8
+ readonly CREATING: "creating";
9
+ readonly COMPLETE: "complete";
10
+ };
11
+ export type GetStartedFlowProps = {
12
+ derivedAccountId: number;
13
+ onRunCreateProject: (args: {
14
+ projectName: string;
15
+ projectDest: string;
16
+ }) => Promise<{
17
+ projectName: string;
18
+ projectDest: string;
19
+ }>;
20
+ initialName?: string;
21
+ initialDest?: string;
22
+ };
23
+ export declare function getGetStartedFlow(props: GetStartedFlowProps): React.ReactNode;
24
+ export declare function GetStartedFlow({ derivedAccountId, onRunCreateProject, initialName, initialDest, }: GetStartedFlowProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { sanitizeFileName, untildify } from '@hubspot/local-dev-lib/path';
3
+ import { Box, Text, useApp, useFocus, useInput } from 'ink';
4
+ import { useCallback, useEffect, useState } from 'react';
5
+ import { commands } from '../../lang/en.js';
6
+ import { GET_STARTED_OPTIONS } from '../../lib/constants.js';
7
+ import { uiAccountDescription } from '../../lib/ui/index.js';
8
+ import { ActionSection } from '../components/ActionSection.js';
9
+ import { BoxWithTitle } from '../components/BoxWithTitle.js';
10
+ import { InputField } from '../components/InputField.js';
11
+ import { SelectInput } from '../components/SelectInput.js';
12
+ import { ACTION_STATUSES } from '../constants.js';
13
+ import { INK_COLORS } from '../styles.js';
14
+ export const DEFAULT_PROJECT_NAME = 'get-started-project';
15
+ export const GET_STARTED_FLOW_OPTIONS = [
16
+ {
17
+ label: commands.getStarted.prompts.options.app,
18
+ value: GET_STARTED_OPTIONS.APP,
19
+ },
20
+ {
21
+ label: commands.getStarted.prompts.options.cmsTheme,
22
+ value: 'CMS_THEME',
23
+ disabled: true,
24
+ },
25
+ {
26
+ label: commands.getStarted.prompts.options.cmsReactModule,
27
+ value: 'CMS_REACT_MODULE',
28
+ disabled: true,
29
+ },
30
+ ];
31
+ export const FLOW_STEPS = {
32
+ SELECT: 'select',
33
+ NAME_INPUT: 'name-input',
34
+ DEST_INPUT: 'dest-input',
35
+ CREATING: 'creating',
36
+ COMPLETE: 'complete',
37
+ };
38
+ export function getGetStartedFlow(props) {
39
+ return _jsx(GetStartedFlow, { ...props });
40
+ }
41
+ export function GetStartedFlow({ derivedAccountId, onRunCreateProject, initialName, initialDest, }) {
42
+ useFocus({ autoFocus: true });
43
+ const { exit } = useApp();
44
+ const accountName = uiAccountDescription(derivedAccountId);
45
+ // Determine initial step based on what was provided via CLI
46
+ const getInitialStep = () => {
47
+ if (initialName && initialDest) {
48
+ return FLOW_STEPS.CREATING;
49
+ }
50
+ else if (initialName) {
51
+ return FLOW_STEPS.DEST_INPUT;
52
+ }
53
+ else if (initialDest) {
54
+ return FLOW_STEPS.NAME_INPUT;
55
+ }
56
+ return FLOW_STEPS.SELECT;
57
+ };
58
+ const hasInitialValues = Boolean(initialName && initialDest);
59
+ const [step, setStep] = useState(getInitialStep());
60
+ const [selectedLabel, setSelectedLabel] = useState(initialName ? commands.getStarted.prompts.options.app : '');
61
+ const [projectName, setProjectName] = useState(initialName || DEFAULT_PROJECT_NAME);
62
+ const [projectDest, setProjectDest] = useState(initialDest
63
+ ? untildify(initialDest)
64
+ : sanitizeFileName(DEFAULT_PROJECT_NAME));
65
+ const [createStatus, setCreateStatus] = useState(initialName ? ACTION_STATUSES.RUNNING : ACTION_STATUSES.IDLE);
66
+ const [createError, setCreateError] = useState('');
67
+ // Only auto-update dest from name if dest wasn't provided via CLI
68
+ useEffect(() => {
69
+ if (!initialDest) {
70
+ setProjectDest(sanitizeFileName(projectName));
71
+ }
72
+ }, [projectName, initialDest]);
73
+ const handleSelect = useCallback((item) => {
74
+ if (item.disabled)
75
+ return;
76
+ setSelectedLabel(item.label);
77
+ setStep(FLOW_STEPS.NAME_INPUT);
78
+ setCreateStatus(ACTION_STATUSES.RUNNING);
79
+ }, []);
80
+ const handleNameSubmit = useCallback(() => {
81
+ setStep(FLOW_STEPS.DEST_INPUT);
82
+ }, []);
83
+ const handleDestChange = useCallback((value) => {
84
+ setProjectDest(value);
85
+ if (createError) {
86
+ setCreateError('');
87
+ }
88
+ }, [createError]);
89
+ const handleDestSubmit = useCallback(async () => {
90
+ setStep(FLOW_STEPS.CREATING);
91
+ setCreateError('');
92
+ setCreateStatus(ACTION_STATUSES.RUNNING);
93
+ try {
94
+ await onRunCreateProject({
95
+ projectName,
96
+ projectDest,
97
+ });
98
+ setCreateStatus(ACTION_STATUSES.DONE);
99
+ setStep(FLOW_STEPS.COMPLETE);
100
+ }
101
+ catch (error) {
102
+ const errorMessage = error instanceof Error
103
+ ? error.message
104
+ : commands.getStarted.v2.unknownError;
105
+ setCreateStatus(ACTION_STATUSES.ERROR);
106
+ setCreateError(errorMessage);
107
+ setStep(FLOW_STEPS.DEST_INPUT);
108
+ }
109
+ }, [onRunCreateProject, projectName, projectDest]);
110
+ // Auto-start project creation when initial values are provided
111
+ useEffect(() => {
112
+ if (hasInitialValues && step === FLOW_STEPS.CREATING) {
113
+ setCreateStatus(ACTION_STATUSES.RUNNING);
114
+ handleDestSubmit();
115
+ }
116
+ }, [hasInitialValues, step, handleDestSubmit]);
117
+ useInput((_, key) => {
118
+ if (step === FLOW_STEPS.COMPLETE && key.return) {
119
+ exit();
120
+ }
121
+ });
122
+ const titleText = commands.getStarted.v2.startTitle;
123
+ const overviewText = commands.getStarted.v2.guideOverview(accountName);
124
+ const projectsText = commands.getStarted.v2.projects;
125
+ const selectPrompt = commands.getStarted.v2.prompts.selectOptionV2;
126
+ const runningProjectCreateText = commands.getStarted.v2.runningProjectCreate;
127
+ return (_jsx(BoxWithTitle, { flexGrow: 1, title: "hs get-started", borderColor: INK_COLORS.HUBSPOT_ORANGE, titleBackgroundColor: INK_COLORS.HUBSPOT_ORANGE, children: _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [_jsx(Text, { bold: true, children: titleText }), step === FLOW_STEPS.SELECT ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: overviewText }), _jsx(Text, { children: projectsText }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: selectPrompt })] }), _jsx(SelectInput, { items: GET_STARTED_FLOW_OPTIONS, onSelect: handleSelect })] })) : (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: `${selectPrompt}` }), _jsx(Text, { color: INK_COLORS.INFO_BLUE, children: selectedLabel })] })), _jsxs(ActionSection, { status: createStatus, statusText: runningProjectCreateText, errorMessage: createError, children: [step !== FLOW_STEPS.SELECT && (_jsx(InputField, { flag: "name", prompt: "Enter your project name", value: projectName, isEditing: step === FLOW_STEPS.NAME_INPUT, onChange: setProjectName, onSubmit: handleNameSubmit })), step !== FLOW_STEPS.SELECT && step !== FLOW_STEPS.NAME_INPUT && (_jsx(InputField, { flag: "dest", prompt: "Choose where to create the project", value: projectDest, isEditing: step === FLOW_STEPS.DEST_INPUT, onChange: handleDestChange, onSubmit: handleDestSubmit }))] }), step === FLOW_STEPS.COMPLETE && (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueDeploy(accountName) })] }))] }) }));
128
+ }
@@ -0,0 +1,10 @@
1
+ export type InputFieldProps = {
2
+ flag: string;
3
+ prompt: string;
4
+ value: string;
5
+ isEditing: boolean;
6
+ onChange: (value: string) => void;
7
+ onSubmit: () => void;
8
+ };
9
+ export declare function InputField({ flag, prompt, value, isEditing, onChange, onSubmit, }: InputFieldProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function getInputField(props: InputFieldProps): React.ReactNode;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ import { INK_COLORS } from '../styles.js';
5
+ export function InputField({ flag, prompt, value, isEditing, onChange, onSubmit, }) {
6
+ return (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, marginBottom: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsxs(Text, { children: ["[--", flag, "] ", prompt] }), _jsx(Text, { color: INK_COLORS.INFO_BLUE, children: _jsx(TextInput, { focus: isEditing, value: value, onChange: onChange, onSubmit: onSubmit }) })] }));
7
+ }
8
+ export function getInputField(props) {
9
+ return _jsx(InputField, { ...props });
10
+ }
@@ -0,0 +1,11 @@
1
+ export type SelectInputItem = {
2
+ label: string;
3
+ value: string;
4
+ disabled?: boolean;
5
+ };
6
+ export type SelectInputProps = {
7
+ items: SelectInputItem[];
8
+ onSelect: (item: SelectInputItem) => void;
9
+ };
10
+ export declare function SelectInput({ items, onSelect }: SelectInputProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function getSelectInput(props: SelectInputProps): React.ReactNode;
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useState } from 'react';
4
+ import { INK_COLORS } from '../styles.js';
5
+ export function SelectInput({ items, onSelect }) {
6
+ const [selectedIndex, setSelectedIndex] = useState(0);
7
+ useInput((_, key) => {
8
+ if (key.upArrow) {
9
+ setSelectedIndex(prevIndex => {
10
+ let newIndex = prevIndex - 1;
11
+ if (newIndex < 0) {
12
+ newIndex = items.length - 1;
13
+ }
14
+ // Skip disabled items
15
+ while (items[newIndex]?.disabled && newIndex !== prevIndex) {
16
+ newIndex--;
17
+ if (newIndex < 0) {
18
+ newIndex = items.length - 1;
19
+ }
20
+ }
21
+ return newIndex;
22
+ });
23
+ }
24
+ if (key.downArrow) {
25
+ setSelectedIndex(prevIndex => {
26
+ let newIndex = prevIndex + 1;
27
+ if (newIndex >= items.length) {
28
+ newIndex = 0;
29
+ }
30
+ // Skip disabled items
31
+ while (items[newIndex]?.disabled && newIndex !== prevIndex) {
32
+ newIndex++;
33
+ if (newIndex >= items.length) {
34
+ newIndex = 0;
35
+ }
36
+ }
37
+ return newIndex;
38
+ });
39
+ }
40
+ if (key.return) {
41
+ const selectedItem = items[selectedIndex];
42
+ if (selectedItem && !selectedItem.disabled) {
43
+ onSelect(selectedItem);
44
+ }
45
+ }
46
+ });
47
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
48
+ const isSelected = index === selectedIndex;
49
+ const isDisabled = item.disabled;
50
+ return (_jsxs(Box, { flexDirection: "row", columnGap: 1, children: [_jsx(Text, { color: isSelected ? INK_COLORS.INFO_BLUE : undefined, children: isSelected ? '❯' : ' ' }), _jsx(Text, { color: isDisabled
51
+ ? INK_COLORS.GRAY
52
+ : isSelected
53
+ ? INK_COLORS.INFO_BLUE
54
+ : undefined, dimColor: isDisabled, children: item.label })] }, item.value));
55
+ }) }));
56
+ }
57
+ export function getSelectInput(props) {
58
+ return _jsx(SelectInput, { ...props });
59
+ }