@botonic/nx-plugin 2.29.0 → 2.31.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 (39) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/executors.json +0 -5
  3. package/package.json +3 -2
  4. package/src/executors/delete-bot/executor.js +0 -2
  5. package/src/executors/deploy-to-hubtype/executor.js +24 -160
  6. package/src/executors/e2e-webchat/botonic-package-publish.spec.ts +7 -11
  7. package/src/executors/integrate-provider/executor.js +0 -2
  8. package/src/executors/login-to-hubtype/executor.js +0 -2
  9. package/src/executors/logout-from-hubtype/executor.js +2 -2
  10. package/src/executors/serve-bot/executor.js +142 -24
  11. package/src/executors/serve-bot/schema.json +13 -5
  12. package/src/generators/bot-app/files/src/client/webchat/index.tsx.template +2 -0
  13. package/src/generators/bot-app/files/src/client/webchat/webchat.tsx.template +111 -0
  14. package/src/generators/bot-app/files/src/server/bot/plugins/flow-builder/index.ts.template +13 -4
  15. package/src/generators/bot-app/files/src/server/bot/plugins/index.ts.template +0 -3
  16. package/src/generators/bot-app/files/src/server/lambda/handler.js.template +1 -1
  17. package/src/generators/bot-app/files/src/server/lambda/package.json +3 -3
  18. package/src/generators/bot-app/files/vite/node.config.ts.template +2 -4
  19. package/src/generators/bot-app/files/vite/webchat.config.ts.template +20 -1
  20. package/src/generators/bot-app/generator.js +6 -5
  21. package/src/generators/bot-app/lilara-version.json +1 -1
  22. package/src/lib/api-service.d.ts +19 -20
  23. package/src/lib/api-service.js +150 -82
  24. package/src/lib/bot-config.d.ts +10 -7
  25. package/src/lib/bot-config.js +5 -1
  26. package/src/lib/constants.d.ts +2 -3
  27. package/src/lib/constants.js +6 -9
  28. package/src/lib/credentials-handler.d.ts +9 -18
  29. package/src/lib/credentials-handler.js +42 -24
  30. package/src/lib/interfaces.d.ts +10 -13
  31. package/src/lib/util/executor-helpers.d.ts +58 -18
  32. package/src/lib/util/executor-helpers.js +501 -102
  33. package/src/plugin.js +6 -15
  34. package/src/executors/deploy-local-runtime/executor.d.ts +0 -5
  35. package/src/executors/deploy-local-runtime/executor.js +0 -144
  36. package/src/executors/deploy-local-runtime/schema.d.js +0 -16
  37. package/src/executors/deploy-local-runtime/schema.json +0 -34
  38. package/src/generators/bot-app/files/src/server/bot/tracking.ts.template +0 -35
  39. package/src/generators/preset/files/package.json +0 -26
package/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## 2.31.0 (2026-06-15)
2
+
3
+ ### 🚀 Features
4
+
5
+ - refactored botonic/HT credentials stored and serve/deploy bots flow refinement ([#934](https://github.com/metis-ai/hubtype-product/pull/934))
6
+ - **lambda:** self-contained bundle with noExternal: true #BLT-2432 ([#936](https://github.com/metis-ai/hubtype-product/pull/936))
7
+ - **serve:** replace deploy-local-runtime with integrated dev session registration ([#921](https://github.com/metis-ai/hubtype-product/pull/921))
8
+
9
+ ### ❤️ Thank You
10
+
11
+ - David Hidalgo @Davidhidalgo
12
+ - Marc Rabat @vanbasten17
13
+
14
+ ## 2.30.0 (2026-06-11)
15
+
16
+ ### 🚀 Features
17
+
18
+ - update botonic-basic-template ([#932](https://github.com/metis-ai/hubtype-product/pull/932))
19
+ - update plugins to last botonicV0 changes #BLT-2370 ([#920](https://github.com/metis-ai/hubtype-product/pull/920))
20
+ - **@botonic/dx-bundling:** derive Lambda noExternal deps programmatically ([#918](https://github.com/metis-ai/hubtype-product/pull/918))
21
+ - support botonic v2 preview in flow builder ([#916](https://github.com/metis-ai/hubtype-product/pull/916))
22
+ - wire plugin-hubtype-analytics into bot apps ([#909](https://github.com/metis-ai/hubtype-product/pull/909))
23
+
24
+ ### 🩹 Fixes
25
+
26
+ - allow multiple, only picking webchat app id ([#896](https://github.com/metis-ai/hubtype-product/pull/896))
27
+
28
+ ### ❤️ Thank You
29
+
30
+ - Marc Rabat @vanbasten17
31
+ - Oriol Raventós @Iru89
32
+
1
33
  ## 2.29.0 (2026-05-12)
2
34
 
3
35
  ### 🚀 Features
package/executors.json CHANGED
@@ -26,11 +26,6 @@
26
26
  "schema": "./src/executors/deploy-to-hubtype/schema.json",
27
27
  "description": "Deploy Botonic app to Hubtype"
28
28
  },
29
- "deploy-local-runtime": {
30
- "implementation": "./src/executors/deploy-local-runtime/executor",
31
- "schema": "./src/executors/deploy-local-runtime/schema.json",
32
- "description": "Deploy Botonic app to Hubtype Local Runtime"
33
- },
34
29
  "integrate-provider": {
35
30
  "implementation": "./src/executors/integrate-provider/executor",
36
31
  "schema": "./src/executors/integrate-provider/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botonic/nx-plugin",
3
- "version": "2.29.0",
3
+ "version": "2.31.0",
4
4
  "description": "Nx plugin for creating Botonic bot applications",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -42,7 +42,8 @@
42
42
  "fs-extra": "^11.1.1",
43
43
  "qs": "^6.11.0",
44
44
  "yaml": "^2.6.1",
45
- "zip-a-folder": "^1.1.0"
45
+ "zip-a-folder": "^1.1.0",
46
+ "zod": "^4.4.3"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "nx": ">=21.0.0"
@@ -22,7 +22,6 @@ __export(executor_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(executor_exports);
24
24
  var import_enquirer = require("enquirer");
25
- var import_path = require("path");
26
25
  var import_api_service = require("../../lib/api-service");
27
26
  var import_executor_helpers = require("../../lib/util/executor-helpers");
28
27
  async function deleteBotExecutor(options, context) {
@@ -32,7 +31,6 @@ async function deleteBotExecutor(options, context) {
32
31
  try {
33
32
  const { targetEnvironment, environmentVariables } = (0, import_executor_helpers.resolveHubtypeEnvironment)(context, options);
34
33
  const botonicApiService = new import_api_service.BotonicAPIService({
35
- workspaceRoot: (0, import_path.resolve)(context.root),
36
34
  environmentVariables,
37
35
  targetEnvironment
38
36
  });
@@ -22,16 +22,12 @@ __export(executor_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(executor_exports);
24
24
  var import_enquirer = require("enquirer");
25
- var import_fs = require("fs");
26
- var import_path = require("path");
27
- var import_zip_a_folder = require("zip-a-folder");
28
25
  var import_api_service = require("../../lib/api-service");
29
- var import_bot_config = require("../../lib/bot-config");
30
26
  var import_executor_helpers = require("../../lib/util/executor-helpers");
31
- var import_file_system = require("../../lib/util/file-system");
32
- var import_system = require("../../lib/util/system");
33
- const BOTONIC_BUNDLE_FILE = "botonic_bundle.zip";
34
- const BOTONIC_TEMP_DIRNAME = "tmp";
27
+ const CREATE_PROD_CHOICE = "Create Production Bot";
28
+ function botLabel(bot) {
29
+ return bot.is_test ? `${bot.name} (Staging bot)` : `${bot.name} (Production bot)`;
30
+ }
35
31
  let PROJECT_ROOT;
36
32
  async function deployToHubtypeExecutor(options, context) {
37
33
  if (!context.projectName) {
@@ -42,7 +38,6 @@ async function deployToHubtypeExecutor(options, context) {
42
38
  const { targetEnvironment, environmentVariables } = (0, import_executor_helpers.resolveHubtypeEnvironment)(context, options);
43
39
  const botonicApiService = new import_api_service.BotonicAPIService({
44
40
  projectRoot: PROJECT_ROOT,
45
- workspaceRoot: (0, import_path.resolve)(context.root),
46
41
  environmentVariables,
47
42
  targetEnvironment
48
43
  });
@@ -69,7 +64,11 @@ async function deployToHubtypeExecutor(options, context) {
69
64
  } else {
70
65
  await handleBotFlow(botonicApiService);
71
66
  }
72
- await deploy(botonicApiService, PROJECT_ROOT, context.projectName);
67
+ await (0, import_executor_helpers.deployBotToHubtype)(
68
+ botonicApiService,
69
+ PROJECT_ROOT,
70
+ context.projectName
71
+ );
73
72
  return { success: true };
74
73
  } catch (error) {
75
74
  return (0, import_executor_helpers.handleExecutorError)(error, "Deployment");
@@ -148,161 +147,26 @@ async function createNewBot(botonicApiService, botName) {
148
147
  }
149
148
  async function selectExistentBot(botonicApiService, bots) {
150
149
  console.log("\u{1F4CB} Select a bot to deploy:\n");
151
- const providersPromises = bots.map((b) => botonicApiService.getProviders(b.id));
152
- const providers = await Promise.all(providersPromises);
153
- const botsWithProviders = bots.map((bot2, index) => ({
154
- ...bot2,
155
- providers: providers[index].data.results
156
- }));
157
150
  const response = await (0, import_enquirer.prompt)({
158
151
  type: "select",
159
- name: "bot_name",
152
+ name: "bot_id",
160
153
  message: "\u{1F916} Please select a bot:",
161
- choices: botsWithProviders.map((bot2) => {
162
- return {
163
- name: bot2.name,
164
- message: bot2.name
165
- };
166
- })
154
+ choices: [
155
+ ...bots.map((bot2) => ({
156
+ name: bot2.id,
157
+ message: botLabel(bot2)
158
+ })),
159
+ { name: CREATE_PROD_CHOICE, message: "+ Create Production Bot" }
160
+ ]
167
161
  });
168
- const selectedBotName = response.bot_name;
169
- const bot = botsWithProviders.find((bot2) => bot2.name === selectedBotName);
162
+ const selectedBotId = response.bot_id;
163
+ if (selectedBotId === CREATE_PROD_CHOICE) {
164
+ await createNewBot(botonicApiService);
165
+ return;
166
+ }
167
+ const bot = bots.find((b) => b.id === selectedBotId);
170
168
  if (bot) {
171
169
  botonicApiService.setCurrentBot(bot);
172
- console.log(`\u2705 Selected bot: ${bot.name}`);
173
- }
174
- }
175
- async function deploy(botonicApiService, projectRoot, projectName) {
176
- console.log("\u{1F528} Preparing your bot for deployment...\n");
177
- const buildOut = await botonicApiService.build({
178
- projectRoot,
179
- projectName
180
- });
181
- if (!buildOut) {
182
- throw new Error("Build failed");
183
- }
184
- const botConfigJson = await import_bot_config.BotConfig.get(projectRoot);
185
- await createBundle(projectRoot);
186
- const { hasDeployErrors } = await deployBundle(
187
- botonicApiService,
188
- botConfigJson
189
- );
190
- await displayDeployResults(botonicApiService, { hasDeployErrors });
191
- (0, import_fs.rmSync)((0, import_path.join)(projectRoot, BOTONIC_BUNDLE_FILE));
192
- (0, import_file_system.removeRecursively)((0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME));
193
- botonicApiService.saveAllCredentials();
194
- }
195
- async function createBundle(projectRoot) {
196
- console.log("\u{1F4E6} Creating deployment bundle...");
197
- if ((0, import_file_system.pathExists)((0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME))) {
198
- (0, import_file_system.removeRecursively)((0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME));
199
- }
200
- const webchatHtmlPath = (0, import_path.join)(projectRoot, "dist", "webchat", "webchat.html");
201
- const webchatIndexPath = (0, import_path.join)(projectRoot, "dist", "webchat", "index.html");
202
- if ((0, import_file_system.pathExists)(webchatHtmlPath)) {
203
- (0, import_fs.renameSync)(webchatHtmlPath, webchatIndexPath);
204
- }
205
- const webviewsHtmlPath = (0, import_path.join)(
206
- projectRoot,
207
- "dist",
208
- "webviews",
209
- "webviews.html"
210
- );
211
- const webviewsIndexPath = (0, import_path.join)(projectRoot, "dist", "webviews", "index.html");
212
- if ((0, import_file_system.pathExists)(webviewsHtmlPath)) {
213
- (0, import_fs.renameSync)(webviewsHtmlPath, webviewsIndexPath);
214
- }
215
- (0, import_file_system.createDir)((0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME));
216
- (0, import_file_system.copy)(
217
- (0, import_path.join)(projectRoot, "dist"),
218
- (0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME, "dist")
219
- );
220
- const zipRes = await import_zip_a_folder.ZipAFolder.zip(
221
- (0, import_path.join)(projectRoot, BOTONIC_TEMP_DIRNAME),
222
- (0, import_path.join)(projectRoot, BOTONIC_BUNDLE_FILE)
223
- );
224
- if (zipRes instanceof Error) {
225
- throw zipRes;
226
- }
227
- const zipStats = (0, import_fs.statSync)((0, import_path.join)(projectRoot, BOTONIC_BUNDLE_FILE));
228
- console.log("\u2705 Bundle created successfully!");
229
- if (zipStats.size >= 20 * 10 ** 6) {
230
- throw new Error(
231
- `Bundle too large: ${(zipStats.size / 1024 / 1024).toFixed(2)}MB (max: 20MB)`
232
- );
233
- }
234
- }
235
- async function deployBundle(botonicApiService, botConfigJson) {
236
- console.log("\u{1F680} Deploying to Hubtype Cloud...");
237
- try {
238
- const deploy2 = await botonicApiService.deployBot(
239
- (0, import_path.join)(PROJECT_ROOT, BOTONIC_BUNDLE_FILE),
240
- botConfigJson
241
- );
242
- if (deploy2.response?.status === 403 || !deploy2.data.deploy_id) {
243
- throw new Error(
244
- `Deploy Botonic Error: ${String(deploy2.response?.data?.status)}`
245
- );
246
- }
247
- console.log("\u23F3 Waiting for deployment to complete...");
248
- while (true) {
249
- await (0, import_system.sleep)(500);
250
- const deployStatus = await botonicApiService.deployStatus(
251
- deploy2.data.deploy_id
252
- );
253
- if (deployStatus.data.is_completed) {
254
- if (deployStatus.data.status === "deploy_status_completed_ok") {
255
- console.log("\u2705 Deployment completed successfully!");
256
- return { hasDeployErrors: false };
257
- } else {
258
- throw new Error(deployStatus.data.error);
259
- }
260
- }
261
- }
262
- } catch (error) {
263
- console.error("\u274C Deployment failed");
264
- let reason = String(error);
265
- if (error.response?.data) {
266
- reason = error.response.data.join("");
267
- }
268
- console.error(`${reason}`);
269
- return { hasDeployErrors: true };
270
- }
271
- }
272
- async function displayDeployResults(botonicApiService, { hasDeployErrors }) {
273
- try {
274
- const providersRes = await botonicApiService.getProviders();
275
- const providers = providersRes.data.results;
276
- if (hasDeployErrors) return false;
277
- if (!providers.length) {
278
- const botId = botonicApiService.botInfo().id;
279
- const accessToken = botonicApiService.getOauth().access_token;
280
- const links = `Now, you can integrate a channel in:
281
- https://app.hubtype.com/bots/${botId}/integrations?access_token=${accessToken}`;
282
- console.log(links);
283
- } else {
284
- displayProviders(providers);
285
- }
286
- return true;
287
- } catch (e) {
288
- console.error(` There was an error getting the providers: ${String(e)}`);
289
- return false;
170
+ console.log(`\u2705 Selected bot: ${botLabel(bot)}`);
290
171
  }
291
172
  }
292
- function displayProviders(providers) {
293
- console.log("\u{1F389} DEPLOYMENT SUCCESSFUL!");
294
- console.log("\u{1F680} Your bot is now live and ready to chat!");
295
- console.log("\u{1F4F1} Your bot is published on:");
296
- providers.forEach((p) => {
297
- if (p.provider === "whatsapp")
298
- console.log(` \u{1F4AC} [WhatsApp] https://wa.me/${p.username}`);
299
- if (p.provider === "facebook")
300
- console.log(` \u{1F4AC} [Facebook] https://m.me/${p.username}`);
301
- if (p.provider === "telegram")
302
- console.log(` \u{1F4AC} [Telegram] https://t.me/${p.username}`);
303
- if (p.provider === "twitter")
304
- console.log(` \u{1F4AC} [Twitter] https://t.me/${p.username}`);
305
- if (p.provider === "generic")
306
- console.log(` \u{1F4AC} [Generic] Your app or website`);
307
- });
308
- }
@@ -14,13 +14,13 @@ test.describe('Botonic Publish E2E', () => {
14
14
 
15
15
  await page.goto('/')
16
16
  await page.waitForLoadState('networkidle')
17
- await page.waitForSelector('button[class*="webchat-trigger"]', {
17
+ await page.waitForSelector('[data-testid="webchat-trigger"]', {
18
18
  timeout: 3000,
19
19
  })
20
20
  })
21
21
 
22
22
  async function ensureWebchatOpen(page: any) {
23
- const trigger = page.locator('button[class*="webchat-trigger"]').first()
23
+ const trigger = page.getByTestId('webchat-trigger')
24
24
  await expect(trigger).toBeVisible({ timeout: 10000 })
25
25
 
26
26
  const ariaExpanded = await trigger.getAttribute('aria-expanded')
@@ -29,16 +29,14 @@ test.describe('Botonic Publish E2E', () => {
29
29
  await page.waitForTimeout(300)
30
30
  }
31
31
 
32
- const container = page
33
- .locator('[class*="webchat-container-module"]')
34
- .first()
32
+ const container = page.getByTestId('webchat-container')
35
33
  await expect(container).toBeVisible({ timeout: 5000 })
36
34
 
37
35
  return { trigger, container }
38
36
  }
39
37
 
40
38
  test('webchat trigger renders and opens webchat', async ({ page }) => {
41
- const trigger = page.locator('button[class*="webchat-trigger"]').first()
39
+ const trigger = page.getByTestId('webchat-trigger')
42
40
  await expect(trigger).toBeVisible({ timeout: 10000 })
43
41
 
44
42
  const ariaExpanded = await trigger.getAttribute('aria-expanded')
@@ -47,9 +45,7 @@ test.describe('Botonic Publish E2E', () => {
47
45
  await page.waitForTimeout(300)
48
46
  }
49
47
 
50
- const container = page
51
- .locator('[class*="webchat-container-module"]')
52
- .first()
48
+ const container = page.getByTestId('webchat-container')
53
49
  await expect(container).toBeVisible({ timeout: 5000 })
54
50
  })
55
51
 
@@ -62,7 +58,7 @@ test.describe('Botonic Publish E2E', () => {
62
58
  await input.fill('hello')
63
59
  await input.press('Enter')
64
60
 
65
- const messages = page.locator('[class*="message-module"]')
61
+ const messages = page.locator('[data-testid^="webchat-message-"]')
66
62
  await expect(messages.first()).toBeVisible({ timeout: 15000 })
67
63
  })
68
64
 
@@ -78,7 +74,7 @@ test.describe('Botonic Publish E2E', () => {
78
74
  await expect(sendButton).toBeVisible()
79
75
  await sendButton.click()
80
76
 
81
- const messages = page.locator('[class*="message-module"]')
77
+ const messages = page.locator('[data-testid^="webchat-message-"]')
82
78
  await expect(messages.first()).toBeVisible({ timeout: 15000 })
83
79
  })
84
80
  })
@@ -22,7 +22,6 @@ __export(executor_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(executor_exports);
24
24
  var import_enquirer = require("enquirer");
25
- var import_path = require("path");
26
25
  var import_api_service = require("../../lib/api-service");
27
26
  var import_executor_helpers = require("../../lib/util/executor-helpers");
28
27
  let PROJECT_ROOT;
@@ -32,7 +31,6 @@ async function integrateProviderExecutor(options, context) {
32
31
  const { targetEnvironment, environmentVariables } = (0, import_executor_helpers.resolveHubtypeEnvironment)(context, options);
33
32
  const botonicApiService = new import_api_service.BotonicAPIService({
34
33
  projectRoot: PROJECT_ROOT,
35
- workspaceRoot: (0, import_path.resolve)(context.root),
36
34
  environmentVariables,
37
35
  targetEnvironment
38
36
  });
@@ -22,7 +22,6 @@ __export(executor_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(executor_exports);
24
24
  var import_enquirer = require("enquirer");
25
- var import_path = require("path");
26
25
  var import_api_service = require("../../lib/api-service");
27
26
  var import_executor_helpers = require("../../lib/util/executor-helpers");
28
27
  async function loginToHubtypeExecutor(options, context) {
@@ -31,7 +30,6 @@ async function loginToHubtypeExecutor(options, context) {
31
30
  const { targetEnvironment, environmentVariables } = (0, import_executor_helpers.resolveHubtypeEnvironment)(context, options);
32
31
  const botonicApiService = new import_api_service.BotonicAPIService({
33
32
  projectRoot,
34
- workspaceRoot: (0, import_path.resolve)(context.root),
35
33
  environmentVariables,
36
34
  targetEnvironment
37
35
  });
@@ -30,12 +30,12 @@ async function logoutFromHubtypeExecutor(options, context) {
30
30
  const homeCredsPath = (0, import_path.join)(
31
31
  (0, import_os.homedir)(),
32
32
  import_constants.BOTONIC_HOME_DIRNAME,
33
- import_constants.GLOBAL_CREDS_FILENAME
33
+ import_constants.ENV_CREDENTIALS_FILENAME
34
34
  );
35
35
  const workspaceCredsPath = (0, import_path.join)(
36
36
  (0, import_path.resolve)(context.root),
37
37
  import_constants.BOTONIC_HOME_DIRNAME,
38
- import_constants.GLOBAL_CREDS_FILENAME
38
+ import_constants.ENV_CREDENTIALS_FILENAME
39
39
  );
40
40
  let removed = false;
41
41
  if (await (0, import_fs_extra.pathExists)(workspaceCredsPath)) {
@@ -70,7 +70,10 @@ function writeToLogFile(line) {
70
70
  }
71
71
  function checkFrontailInstalled(cwd) {
72
72
  try {
73
- (0, import_child_process.execSync)("npx --no frontail --help", { stdio: "pipe", cwd });
73
+ (0, import_child_process.execSync)(`${cwd}/node_modules/.bin/frontail --help`, {
74
+ stdio: "pipe",
75
+ cwd
76
+ });
74
77
  return true;
75
78
  } catch {
76
79
  return false;
@@ -86,10 +89,8 @@ function startFrontail(logPath, port, cwd) {
86
89
  return null;
87
90
  }
88
91
  const frontailProcess = (0, import_child_process.spawn)(
89
- "npx",
92
+ `${cwd}/node_modules/.bin/frontail`,
90
93
  [
91
- "--no",
92
- "frontail",
93
94
  "--port",
94
95
  port.toString(),
95
96
  "--theme",
@@ -205,33 +206,60 @@ async function serveBotExecutor(options, context) {
205
206
  `${colors.dim}[lambda] No previous Lambda container to clean up.${colors.reset}`
206
207
  );
207
208
  }
208
- const logViewerEnabled = options.logViewer === true;
209
209
  logFilePath = null;
210
210
  const logViewerPort = options.logViewerPort ?? 9001;
211
211
  const webchatDevPort = tryReadWebchatDevPort(fullProjectRoot);
212
- let frontailProcess = null;
213
- if (logViewerEnabled) {
214
- logFilePath = initLogFile(fullProjectRoot);
215
- frontailProcess = startFrontail(logFilePath, logViewerPort, context.root);
216
- }
217
- const logViewerInfo = logViewerEnabled ? `${colors.dim}Webchat:${colors.reset} ${colors.bold}http://localhost:${webchatDevPort}/?logs=${logViewerPort}${colors.reset} (with logs panel)
218
- ${colors.dim}Log viewer:${colors.reset} http://localhost:${logViewerPort}` : "";
212
+ logFilePath = initLogFile(fullProjectRoot);
213
+ const frontailProcess = startFrontail(
214
+ logFilePath,
215
+ logViewerPort,
216
+ context.root
217
+ );
218
+ const logViewerInfo = `${colors.dim}Webchat:${colors.reset} ${colors.bold}http://localhost:${webchatDevPort}/?logs=${logViewerPort}${colors.reset} (with logs panel)
219
+ ${colors.dim}Log viewer:${colors.reset} http://localhost:${logViewerPort}`;
219
220
  const useTunnel = true;
220
221
  const tunnelPort = options.tunnelPort ?? 3001;
221
222
  let effectiveLambdaEndpoint = options.lambdaEndpoint;
222
223
  let startLambda = !options.skipLambda && !effectiveLambdaEndpoint;
223
224
  let tunnelDeployResult;
225
+ let devSessionLambdaFunctionName;
226
+ let registeredTunnelUrl;
227
+ let deployedBotId;
228
+ let selectedBotId;
229
+ let deployedApiUrl;
230
+ let deployedAccessToken;
231
+ let deployedRefreshToken;
232
+ let deployedClientId;
224
233
  if (useTunnel) {
225
234
  await (0, import_executor_helpers.ensureHubtypeLoginBeforeTunnel)(context, fullProjectRoot, {
226
235
  env: options.env,
227
236
  configuration
228
237
  });
229
- await (0, import_executor_helpers.ensureLocalRuntimeBotBeforeTunnel)(context, fullProjectRoot, {
230
- env: options.env,
231
- configuration
232
- });
238
+ try {
239
+ selectedBotId = await (0, import_executor_helpers.selectBotForServe)(context, fullProjectRoot, {
240
+ env: options.env,
241
+ configuration,
242
+ botName: options.botName
243
+ });
244
+ } catch (botDeployErr) {
245
+ if ((0, import_executor_helpers.isBotDeployedRestartRequired)(botDeployErr)) {
246
+ if (frontailProcess) {
247
+ frontailProcess.kill("SIGTERM");
248
+ }
249
+ if (process.stdin.isTTY) {
250
+ try {
251
+ process.stdin.setRawMode(false);
252
+ } catch {
253
+ }
254
+ }
255
+ process.stdin.pause();
256
+ process.stdin.unref();
257
+ return { success: true };
258
+ }
259
+ throw botDeployErr;
260
+ }
233
261
  console.log(
234
- `${colors.dim}Starting tunnel flow (Lambda + cloudflared + deploy_local_runtime)...${colors.reset}
262
+ `${colors.dim}Starting tunnel flow (Lambda + cloudflared + dev session registration)...${colors.reset}
235
263
  `
236
264
  );
237
265
  }
@@ -256,18 +284,79 @@ ${colors.dim}Log viewer:${colors.reset} http://localhost:${logViewerPort}` :
256
284
  console.log(`${colors.dim}Tunnel URL: ${tunnelUrl}${colors.reset}
257
285
  `);
258
286
  console.log(
259
- `${colors.dim}Registering tunnel with backend (deploy_local_runtime)...${colors.reset}`
287
+ `${colors.dim}Setting up dev bot and writing dev session env vars...${colors.reset}`
260
288
  );
261
289
  const deployResult = await (0, import_executor_helpers.performDeployLocalRuntimeWithEndpoint)(
262
290
  context,
263
291
  fullProjectRoot,
264
292
  tunnelUrl,
265
- { env: options.env, configuration }
293
+ {
294
+ env: options.env,
295
+ configuration,
296
+ whatsappPhone: options.phone,
297
+ selectedBotId
298
+ }
266
299
  );
267
300
  tunnelDeployResult = {
268
301
  targetEnvironment: deployResult.targetEnvironment,
269
- environmentVariables: deployResult.environmentVariables
302
+ environmentVariables: deployResult.environmentVariables,
303
+ teardownWebchat: deployResult.teardownWebchat
270
304
  };
305
+ devSessionLambdaFunctionName = deployResult.lambdaFunctionName;
306
+ registeredTunnelUrl = tunnelUrl;
307
+ deployedBotId = deployResult.botId ?? "";
308
+ deployedApiUrl = deployResult.apiUrl ?? "https://api.hubtype.com";
309
+ deployedAccessToken = deployResult.accessToken ?? "";
310
+ deployedRefreshToken = deployResult.refreshToken ?? "";
311
+ deployedClientId = deployResult.clientId ?? "";
312
+ const resolvedEnv = ["local", "dev", "dev2", "qa", "prod"].includes(deployResult.targetEnvironment) ? deployResult.targetEnvironment : "local";
313
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
314
+ fullProjectRoot,
315
+ "VITE_DEV_SESSION_URL",
316
+ tunnelUrl,
317
+ resolvedEnv
318
+ );
319
+ if (devSessionLambdaFunctionName) {
320
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
321
+ fullProjectRoot,
322
+ "VITE_DEV_LAMBDA_FUNCTION_NAME",
323
+ devSessionLambdaFunctionName,
324
+ resolvedEnv
325
+ );
326
+ }
327
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
328
+ fullProjectRoot,
329
+ "VITE_DEV_BOT_ID",
330
+ deployResult.botId ?? "",
331
+ resolvedEnv
332
+ );
333
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
334
+ fullProjectRoot,
335
+ "VITE_DEV_API_URL",
336
+ deployResult.apiUrl ?? "https://api.hubtype.com",
337
+ resolvedEnv
338
+ );
339
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
340
+ fullProjectRoot,
341
+ "VITE_DEV_ACCESS_TOKEN",
342
+ deployResult.accessToken ?? "",
343
+ resolvedEnv
344
+ );
345
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
346
+ fullProjectRoot,
347
+ "VITE_DEV_REFRESH_TOKEN",
348
+ deployResult.refreshToken ?? "",
349
+ resolvedEnv
350
+ );
351
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
352
+ fullProjectRoot,
353
+ "VITE_DEV_CLIENT_ID",
354
+ deployResult.clientId ?? "",
355
+ resolvedEnv
356
+ );
357
+ console.log(
358
+ `${colors.dim}VITE_DEV_SESSION_URL written to .env.${resolvedEnv}${colors.reset}`
359
+ );
271
360
  effectiveLambdaEndpoint = tunnelUrl;
272
361
  startLambda = false;
273
362
  }
@@ -312,7 +401,7 @@ ${colors.webviews}[webviews]${colors.reset} - Webviews Vite dev server
312
401
  `);
313
402
  if (useExternalLambda && !useTunnel) {
314
403
  console.log(
315
- `${colors.dim}\u{1F4A1} Ensure you have run deploy_local_runtime with this URL so the backend can invoke your Lambda.${colors.reset}
404
+ `${colors.dim}\u{1F4A1} Ensure you have run the serve target first to register your dev session.${colors.reset}
316
405
  `
317
406
  );
318
407
  }
@@ -340,13 +429,23 @@ ${colors.webviews}[webviews]${colors.reset} - Webviews Vite dev server
340
429
  }
341
430
  const noOpenEnv = options.open === false ? "VITE_SERVE_OPEN=false " : "";
342
431
  const viteEnvAppId = `VITE_HUBTYPE_APP_ID=${effectiveAppId}`;
343
- const logViewerEnv = logViewerEnabled ? `LOG_VIEWER_PORT=${logViewerPort} ` : "";
344
- const webchatCommand = `${noOpenEnv}${logViewerEnv}${viteEnvAppId} TARGET_APP=webchat MODE=${configuration} vite --config ${configPath}`;
432
+ const viteDevSessionEnv = registeredTunnelUrl && devSessionLambdaFunctionName ? `VITE_DEV_SESSION_URL=${registeredTunnelUrl} VITE_DEV_LAMBDA_FUNCTION_NAME=${devSessionLambdaFunctionName} ` : "";
433
+ const viteDevCredentialsEnv = deployedBotId ? [
434
+ `VITE_DEV_BOT_ID=${deployedBotId}`,
435
+ `VITE_DEV_API_URL=${deployedApiUrl ?? "https://api.hubtype.com"}`,
436
+ `VITE_DEV_ACCESS_TOKEN=${deployedAccessToken ?? ""}`,
437
+ `VITE_DEV_REFRESH_TOKEN=${deployedRefreshToken ?? ""}`,
438
+ `VITE_DEV_CLIENT_ID=${deployedClientId ?? ""}`
439
+ ].join(" ") + " " : "";
440
+ const logViewerEnv = `LOG_VIEWER_PORT=${logViewerPort} `;
441
+ const botHasWhatsapp = deployedBotId ? (0, import_executor_helpers.selectedBotHasActiveWhatsapp)(deployedBotId) : false;
442
+ const whatsappPanelEnv = botHasWhatsapp ? `BOT_HAS_WHATSAPP=1 ${options.phone ? `WHATSAPP_PANEL_PHONE=${options.phone} ` : ""}` : "";
443
+ const webchatCommand = `${noOpenEnv}${logViewerEnv}${viteDevSessionEnv}${viteDevCredentialsEnv}${whatsappPanelEnv}${viteEnvAppId} TARGET_APP=webchat MODE=${configuration} vite --config ${configPath}`;
345
444
  processes.push(
346
445
  spawnProcess(webchatCommand, "webchat", colors.webchat, fullProjectRoot)
347
446
  );
348
447
  if (!options.skipWebviews) {
349
- const webviewsCommand = `${noOpenEnv}${viteEnvAppId} TARGET_APP=webviews MODE=${configuration} vite --config ${configPath}`;
448
+ const webviewsCommand = `${noOpenEnv}${viteDevSessionEnv}${viteEnvAppId} TARGET_APP=webviews MODE=${configuration} vite --config ${configPath}`;
350
449
  processes.push(
351
450
  spawnProcess(
352
451
  webviewsCommand,
@@ -364,6 +463,22 @@ ${colors.webviews}[webviews]${colors.reset} - Webviews Vite dev server
364
463
  `
365
464
  ${colors.bold}\u{1F6D1} Shutting down all processes...${colors.reset}`
366
465
  );
466
+ try {
467
+ const envConfig = ["local", "dev", "dev2", "qa", "prod"].includes(tunnelDeployResult?.targetEnvironment ?? "") ? tunnelDeployResult.targetEnvironment : "local";
468
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
469
+ fullProjectRoot,
470
+ "VITE_DEV_SESSION_URL",
471
+ "",
472
+ envConfig
473
+ );
474
+ (0, import_executor_helpers.writeEnvVarToEnvFile)(
475
+ fullProjectRoot,
476
+ "VITE_DEV_LAMBDA_FUNCTION_NAME",
477
+ "",
478
+ envConfig
479
+ );
480
+ } catch {
481
+ }
367
482
  processes.forEach(({ name, color, process: proc }) => {
368
483
  console.log(`${color}[${name}]${colors.reset} Stopping...`);
369
484
  proc.kill("SIGTERM");
@@ -405,6 +520,9 @@ ${colors.bold}\u{1F6D1} Shutting down all processes...${colors.reset}`
405
520
  });
406
521
  });
407
522
  } catch (error) {
523
+ if ((0, import_executor_helpers.isBotDeployedRestartRequired)(error)) {
524
+ return { success: true };
525
+ }
408
526
  const errorMessage = error instanceof Error ? error.message : String(error);
409
527
  console.error(`\u274C Error starting dev server:`, errorMessage);
410
528
  return { success: false };