@khester/create-dynamics-app 2.6.6 → 2.8.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 (66) hide show
  1. package/README.md +3 -0
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +87 -6
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/scaffold.d.ts +2 -0
  10. package/dist/scaffold.d.ts.map +1 -1
  11. package/dist/scaffold.js +4 -0
  12. package/dist/scaffold.js.map +1 -1
  13. package/dist/utils/copyTemplate.d.ts.map +1 -1
  14. package/dist/utils/copyTemplate.js +26 -7
  15. package/dist/utils/copyTemplate.js.map +1 -1
  16. package/package.json +2 -2
  17. package/templates/teams-app/README.md +32 -9
  18. package/templates/teams-app/{appPackage/manifest.json → _variants/tab.basic/appPackage/manifest.template.json} +6 -5
  19. package/templates/teams-app/_variants/tab.dashboard/appPackage/manifest.template.json +38 -0
  20. package/templates/teams-app/_variants/tab.dashboard/auth.html +15 -0
  21. package/templates/teams-app/_variants/tab.dashboard/env.local.template +3 -0
  22. package/templates/teams-app/_variants/tab.dashboard/src/App.tsx +49 -0
  23. package/templates/teams-app/_variants/tab.dashboard/src/auth-bridge.ts +52 -0
  24. package/templates/teams-app/_variants/tab.dashboard/src/components/Dashboard.tsx +33 -0
  25. package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/StatsWidget.tsx +25 -0
  26. package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/TasksWidget.tsx +20 -0
  27. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useHashRoute.ts +16 -0
  28. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTabConfig.ts +70 -0
  29. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTeamsAuth.ts +121 -0
  30. package/templates/teams-app/_variants/tab.dashboard/src/pages/Config.tsx +118 -0
  31. package/templates/teams-app/_variants/tab.dashboard/src/pages/Remove.tsx +12 -0
  32. package/templates/teams-app/_variants/tab.dashboard/src/vite-env.d.ts +10 -0
  33. package/templates/teams-app/_variants/tab.dashboard/vite.config.ts +19 -0
  34. package/templates/teams-app/_variants/tab.sso/appPackage/manifest.template.json +31 -0
  35. package/templates/teams-app/_variants/tab.sso/auth.html +15 -0
  36. package/templates/teams-app/_variants/tab.sso/env.local.template +3 -0
  37. package/templates/teams-app/_variants/tab.sso/src/App.tsx +57 -0
  38. package/templates/teams-app/_variants/tab.sso/src/auth-bridge.ts +52 -0
  39. package/templates/teams-app/_variants/tab.sso/src/hooks/useTeamsAuth.ts +121 -0
  40. package/templates/teams-app/_variants/tab.sso/src/vite-env.d.ts +10 -0
  41. package/templates/teams-app/_variants/tab.sso/vite.config.ts +19 -0
  42. package/templates/teams-app/appPackage/color.png +0 -0
  43. package/templates/teams-app/appPackage/outline.png +0 -0
  44. package/templates/teams-app/gitignore +2 -0
  45. package/templates/teams-app/package.json +1 -1
  46. package/templates/teams-bot/.env.example +18 -0
  47. package/templates/teams-bot/README.md +52 -0
  48. package/templates/teams-bot/_variants/bot.echo/appPackage/manifest.template.json +25 -0
  49. package/templates/teams-bot/_variants/bot.echo/src/bot/TeamsBot.ts +142 -0
  50. package/templates/teams-bot/_variants/bot.echo/src/bot/welcomeCard.ts +134 -0
  51. package/templates/teams-bot/_variants/bot.echo/src/index.ts +59 -0
  52. package/templates/teams-bot/_variants/bot.echo/src/mock/mockMode.ts +104 -0
  53. package/templates/teams-bot/_variants/bot.notification/appPackage/manifest.template.json +25 -0
  54. package/templates/teams-bot/_variants/bot.notification/src/bot/NotificationBot.ts +54 -0
  55. package/templates/teams-bot/_variants/bot.notification/src/index.ts +136 -0
  56. package/templates/teams-bot/_variants/bot.notification/src/mock/mockMode.ts +55 -0
  57. package/templates/teams-bot/_variants/bot.notification/src/storage/conversationStore.ts +84 -0
  58. package/templates/teams-bot/appPackage/color.png +0 -0
  59. package/templates/teams-bot/appPackage/outline.png +0 -0
  60. package/templates/teams-bot/gitignore +8 -0
  61. package/templates/teams-bot/nodemon.json +6 -0
  62. package/templates/teams-bot/package.json +24 -0
  63. package/templates/teams-bot/src/adapters/botAdapter.ts +45 -0
  64. package/templates/teams-bot/src/config.ts +34 -0
  65. package/templates/teams-bot/tsconfig.json +17 -0
  66. /package/templates/teams-app/{src → _variants/tab.basic/src}/App.tsx +0 -0
@@ -0,0 +1,52 @@
1
+ import { app, authentication } from '@microsoft/teams-js';
2
+ import { PublicClientApplication } from '@azure/msal-browser';
3
+
4
+ /**
5
+ * Auth bridge — loaded by auth.html inside the window Teams opens for
6
+ * `authentication.authenticate()`. MSAL's popup flow is blocked inside the Teams
7
+ * tab iframe, so interactive sign-in happens here: loginRedirect → back to this
8
+ * page → handleRedirectPromise → notifySuccess back to the tab.
9
+ */
10
+ const setStatus = (msg: string): void => {
11
+ const el = document.getElementById('status');
12
+ if (el) el.textContent = msg;
13
+ };
14
+
15
+ (async () => {
16
+ try {
17
+ await app.initialize();
18
+
19
+ const clientId = import.meta.env.VITE_CLIENT_ID ?? '';
20
+ const tenantId = import.meta.env.VITE_TENANT_ID ?? 'common';
21
+ if (!clientId) throw new Error('VITE_CLIENT_ID is not set — copy env.local.template to .env.local');
22
+
23
+ const pca = new PublicClientApplication({
24
+ auth: {
25
+ clientId,
26
+ authority: `https://login.microsoftonline.com/${tenantId}`,
27
+ redirectUri: `${window.location.origin}/auth.html`,
28
+ },
29
+ cache: { cacheLocation: 'localStorage' },
30
+ });
31
+ await pca.initialize();
32
+
33
+ setStatus('Completing sign-in…');
34
+ const response = await pca.handleRedirectPromise();
35
+ if (response?.account) {
36
+ setStatus(`Signed in as ${response.account.username}`);
37
+ authentication.notifySuccess(response.account.username);
38
+ return;
39
+ }
40
+
41
+ setStatus('Redirecting to sign in…');
42
+ await pca.loginRedirect({ scopes: ['User.Read'] });
43
+ } catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ setStatus(`Sign-in failed: ${msg}`);
46
+ try {
47
+ authentication.notifyFailure(msg);
48
+ } catch {
49
+ // Outside a Teams auth window — the status line above is the surface.
50
+ }
51
+ }
52
+ })();
@@ -0,0 +1,121 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { app, authentication } from '@microsoft/teams-js';
3
+ import { PublicClientApplication, type AccountInfo } from '@azure/msal-browser';
4
+
5
+ /**
6
+ * Teams-first auth: try Teams SSO (`authentication.getAuthToken()`), fall back to
7
+ * MSAL. Inside the Teams iframe, interactive sign-in must go through the auth
8
+ * bridge (`auth.html`) — `loginPopup` is blocked there; outside Teams (plain
9
+ * browser tab) MSAL redirect works directly.
10
+ *
11
+ * VITE_CLIENT_ID / VITE_TENANT_ID come from `.env.local` (see env.local.template);
12
+ * `teams-app-manager.sh identity` writes them after registering the Entra app.
13
+ */
14
+ const clientId = import.meta.env.VITE_CLIENT_ID ?? '';
15
+ const tenantId = import.meta.env.VITE_TENANT_ID ?? 'common';
16
+
17
+ const msalInstance = new PublicClientApplication({
18
+ auth: {
19
+ clientId,
20
+ authority: `https://login.microsoftonline.com/${tenantId}`,
21
+ redirectUri: window.location.origin,
22
+ },
23
+ cache: { cacheLocation: 'localStorage' },
24
+ });
25
+
26
+ export interface AuthState {
27
+ token: string | null;
28
+ account: AccountInfo | null;
29
+ inTeams: boolean;
30
+ loading: boolean;
31
+ error: Error | null;
32
+ }
33
+
34
+ export function useTeamsAuth(): AuthState & { login: () => Promise<void> } {
35
+ const [state, setState] = useState<AuthState>({
36
+ token: null,
37
+ account: null,
38
+ inTeams: false,
39
+ loading: true,
40
+ error: null,
41
+ });
42
+
43
+ const getToken = useCallback(async () => {
44
+ // 1. Teams SSO — works once the app registration exposes api://{host}/{clientId}
45
+ // with the Teams client ids pre-authorized.
46
+ try {
47
+ await app.initialize();
48
+ const ssoToken = await authentication.getAuthToken();
49
+ setState({ token: ssoToken, account: null, inTeams: true, loading: false, error: null });
50
+ return;
51
+ } catch {
52
+ // Not in Teams, or SSO not consented yet — fall through to MSAL.
53
+ }
54
+
55
+ // 2. MSAL silent — a previous interactive sign-in left an account in the cache.
56
+ try {
57
+ await msalInstance.initialize();
58
+ const redirect = await msalInstance.handleRedirectPromise().catch(() => null);
59
+ const account = redirect?.account ?? msalInstance.getAllAccounts()[0];
60
+ if (account) {
61
+ const result = await msalInstance.acquireTokenSilent({
62
+ scopes: ['User.Read'],
63
+ account,
64
+ });
65
+ setState({
66
+ token: result.accessToken,
67
+ account: result.account,
68
+ inTeams: false,
69
+ loading: false,
70
+ error: null,
71
+ });
72
+ return;
73
+ }
74
+ setState((prev) => ({ ...prev, loading: false }));
75
+ } catch (error) {
76
+ setState((prev) => ({ ...prev, loading: false, error: error as Error }));
77
+ }
78
+ }, []);
79
+
80
+ /** Interactive sign-in. In Teams → auth bridge popup; in a browser → MSAL redirect. */
81
+ const login = useCallback(async () => {
82
+ if (!clientId) {
83
+ setState((prev) => ({
84
+ ...prev,
85
+ error: new Error('VITE_CLIENT_ID is not set — copy env.local.template to .env.local'),
86
+ }));
87
+ return;
88
+ }
89
+ let inTeams = false;
90
+ try {
91
+ await app.initialize();
92
+ inTeams = true;
93
+ } catch {
94
+ // Plain browser tab.
95
+ }
96
+
97
+ try {
98
+ if (inTeams) {
99
+ // Teams opens auth.html in a real window where MSAL's redirect flow is
100
+ // allowed; the bridge calls notifySuccess/notifyFailure when done.
101
+ await authentication.authenticate({
102
+ url: `${window.location.origin}/auth.html`,
103
+ width: 600,
104
+ height: 535,
105
+ });
106
+ await getToken();
107
+ return;
108
+ }
109
+ await msalInstance.initialize();
110
+ await msalInstance.loginRedirect({ scopes: ['User.Read'] });
111
+ } catch (error) {
112
+ setState((prev) => ({ ...prev, error: error as Error }));
113
+ }
114
+ }, [getToken]);
115
+
116
+ useEffect(() => {
117
+ void getToken();
118
+ }, [getToken]);
119
+
120
+ return { ...state, login };
121
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_CLIENT_ID?: string;
5
+ readonly VITE_TENANT_ID?: string;
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv;
10
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath, URL } from 'node:url';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ // Teams tabs must be served over HTTPS; `vite --https` or a tunnel in dev.
8
+ server: { port: 3000 },
9
+ build: {
10
+ outDir: 'dist',
11
+ rollupOptions: {
12
+ // MPA: the tab plus the auth bridge Teams opens for interactive sign-in.
13
+ input: {
14
+ main: fileURLToPath(new URL('./index.html', import.meta.url)),
15
+ auth: fileURLToPath(new URL('./auth.html', import.meta.url)),
16
+ },
17
+ },
18
+ },
19
+ });
@@ -1,5 +1,7 @@
1
1
  node_modules/
2
2
  dist/
3
+ build/
3
4
  .env
4
5
  .env.local
6
+ .teams/
5
7
  *.log
@@ -14,7 +14,7 @@
14
14
  "react": "^18.2.0",
15
15
  "react-dom": "^18.2.0",
16
16
  "@fluentui/react-components": "^9.46.2",
17
- "@microsoft/teams-js": "^2.19.0"
17
+ "@microsoft/teams-js": "^2.24.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/react": "^18.2.0",
@@ -0,0 +1,18 @@
1
+ # Bot Framework credentials — written to .env.local by `teams-app-manager.sh identity`
2
+ # (which .env.local overrides; this file documents the full set). Never commit real values.
3
+ MICROSOFT_APP_ID=
4
+ MICROSOFT_APP_PASSWORD=
5
+ MICROSOFT_APP_TENANT_ID=
6
+ # SingleTenant (default) or MultiTenant — must match the Azure Bot resource's app type.
7
+ MICROSOFT_APP_TYPE=SingleTenant
8
+
9
+ # Shared secret required in the X-Notify-Key header by POST /api/notify (notification bots).
10
+ NOTIFY_API_KEY=
11
+
12
+ # Where conversation references persist (notification bots). On Azure App Service use
13
+ # /home/data/conversations.json so subscriptions survive restarts and redeploys.
14
+ CONVERSATION_STORE_PATH=./.data/conversations.json
15
+
16
+ PORT=3978
17
+ # Mock mode runs the bot without any Azure registration (try: npm run dev -- --interactive)
18
+ MOCK_MODE=true
@@ -0,0 +1,52 @@
1
+ # {{projectName}}
2
+
3
+ A Microsoft Teams bot built with the Bot Framework SDK v4 (TypeScript + Express),
4
+ scaffolded by `create-dynamics-app --artifact teams-bot`.
5
+
6
+ ## Local development (no Azure needed)
7
+
8
+ ```bash
9
+ cp .env.example .env # MOCK_MODE=true is the default
10
+ npm install
11
+ npm run dev -- --interactive # console chat loop against the bot logic
12
+ npm run dev # or: run the HTTP server on http://localhost:3978
13
+ ```
14
+
15
+ Mock mode drives the bot through the Bot Framework `TestAdapter` — no app registration,
16
+ no tunnel. The HTTP server exposes:
17
+
18
+ | Endpoint | Purpose |
19
+ |----------|---------|
20
+ | `POST /api/messages` | Bot Framework messaging endpoint |
21
+ | `GET /health` | Health probe (used by the deploy pipeline) |
22
+ | `POST /api/notify` | Proactive notifications (notification bots only; requires the `X-Notify-Key` header) |
23
+
24
+ ## Ship it to Azure + Teams
25
+
26
+ The workspace pipeline provisions everything (App Service hosting, Entra app + secret,
27
+ Azure Bot resource, Teams channel, app package):
28
+
29
+ ```bash
30
+ teams-app-manager.sh up --project . --sideload
31
+ ```
32
+
33
+ Or run the stages individually: `hosting` → `identity` → `deploy` → `package` → `sideload`.
34
+ The packaged zip lands in `build/` and is uploaded to Teams via the admin center or
35
+ "Upload a custom app". See `msft/05-m365/docs/teams-bot-runbook.md` in the workspace.
36
+
37
+ ## Configuration
38
+
39
+ All settings come from env vars (see `.env.example`). In Azure they are App Service
40
+ app settings; locally they live in `.env` / `.env.local` (the identity stage writes
41
+ credentials to `.env.local`, which takes precedence).
42
+
43
+ Key settings: the Bot Framework credential triple (app id / password / tenant id), the
44
+ app type (SingleTenant default — must match the Azure Bot resource), the notify API key,
45
+ and the conversation-store path (notification bots persist subscriber references there;
46
+ on App Service use `/home/data/conversations.json` so they survive restarts).
47
+
48
+ ## Manifest
49
+
50
+ `appPackage/manifest.template.json` keeps two deploy-time placeholders — the Teams app
51
+ id and the bot id — that `teams-app-manager.sh package` renders from the provisioning
52
+ state; everything else is baked at scaffold time.
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
3
+ "manifestVersion": "1.19",
4
+ "id": "{{TEAMS_APP_ID}}",
5
+ "version": "1.0.0",
6
+ "developer": {
7
+ "name": "Contoso",
8
+ "websiteUrl": "https://example.com",
9
+ "privacyUrl": "https://example.com/privacy",
10
+ "termsOfUseUrl": "https://example.com/terms"
11
+ },
12
+ "name": { "short": "{{projectName}}", "full": "{{projectName}}" },
13
+ "description": { "short": "{{projectName}} bot", "full": "{{projectName}} — a Microsoft Teams bot that echoes messages and responds to commands" },
14
+ "icons": { "color": "color.png", "outline": "outline.png" },
15
+ "accentColor": "#6264A7",
16
+ "bots": [
17
+ {
18
+ "botId": "{{BOT_ID}}",
19
+ "scopes": ["personal", "team", "groupChat"],
20
+ "supportsFiles": false,
21
+ "isNotificationOnly": false
22
+ }
23
+ ],
24
+ "permissions": ["identity"]
25
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ TeamsActivityHandler,
3
+ TurnContext,
4
+ MessageFactory,
5
+ TeamsInfo,
6
+ AdaptiveCardInvokeValue,
7
+ AdaptiveCardInvokeResponse,
8
+ } from 'botbuilder';
9
+ import { createWelcomeCard, createHelpCard, createAboutCard } from './welcomeCard';
10
+
11
+ export class TeamsBot extends TeamsActivityHandler {
12
+ constructor() {
13
+ super();
14
+
15
+ // Handle incoming messages
16
+ this.onMessage(async (context: TurnContext, next: () => Promise<void>) => {
17
+ await this.handleMessage(context);
18
+ await next();
19
+ });
20
+
21
+ // Handle members added (welcome message)
22
+ this.onMembersAdded(async (context: TurnContext, next: () => Promise<void>) => {
23
+ await this.handleMembersAdded(context);
24
+ await next();
25
+ });
26
+
27
+ // Handle members removed
28
+ this.onMembersRemoved(async (context: TurnContext, next: () => Promise<void>) => {
29
+ await this.handleMembersRemoved(context);
30
+ await next();
31
+ });
32
+ }
33
+
34
+ private async handleMessage(context: TurnContext): Promise<void> {
35
+ // Remove bot mention from message
36
+ const text = this.removeMentionText(context);
37
+ const command = text.toLowerCase().trim();
38
+
39
+ console.log(`Received message: "${text}"`);
40
+
41
+ // Handle commands
42
+ switch (command) {
43
+ case 'help':
44
+ await context.sendActivity({
45
+ attachments: [createHelpCard()],
46
+ });
47
+ break;
48
+
49
+ case 'about':
50
+ await context.sendActivity({
51
+ attachments: [createAboutCard()],
52
+ });
53
+ break;
54
+
55
+ default:
56
+ // Echo the message back
57
+ if (command.startsWith('echo ')) {
58
+ const echoText = text.substring(5);
59
+ await context.sendActivity(MessageFactory.text(`Echo: ${echoText}`));
60
+ } else {
61
+ await context.sendActivity(MessageFactory.text(`You said: "${text}"`));
62
+ }
63
+ break;
64
+ }
65
+ }
66
+
67
+ private async handleMembersAdded(context: TurnContext): Promise<void> {
68
+ const membersAdded = context.activity.membersAdded || [];
69
+ const botId = context.activity.recipient.id;
70
+
71
+ for (const member of membersAdded) {
72
+ // Only send welcome when the bot is added, not when users join
73
+ if (member.id !== botId) {
74
+ continue;
75
+ }
76
+
77
+ // Get the user's name
78
+ let userName = 'there';
79
+ try {
80
+ const members = await TeamsInfo.getMembers(context);
81
+ if (members && members.length > 0) {
82
+ // Find the user who added the bot (typically the first non-bot member)
83
+ const user = members.find((m) => m.id !== botId);
84
+ if (user) {
85
+ userName = user.name || user.givenName || 'there';
86
+ }
87
+ }
88
+ } catch (error) {
89
+ console.log('Could not get team members, using default greeting');
90
+ }
91
+
92
+ // Send welcome card
93
+ await context.sendActivity({
94
+ attachments: [createWelcomeCard(userName)],
95
+ });
96
+ }
97
+ }
98
+
99
+ private async handleMembersRemoved(context: TurnContext): Promise<void> {
100
+ const membersRemoved = context.activity.membersRemoved || [];
101
+
102
+ for (const member of membersRemoved) {
103
+ console.log(`Member removed: ${member.name || member.id}`);
104
+ }
105
+ }
106
+
107
+ // Handle Adaptive Card submit actions
108
+ protected async onAdaptiveCardInvoke(
109
+ context: TurnContext,
110
+ invokeValue: AdaptiveCardInvokeValue
111
+ ): Promise<AdaptiveCardInvokeResponse> {
112
+ const action = invokeValue.action?.data?.action;
113
+
114
+ if (action === 'hello') {
115
+ await context.sendActivity(MessageFactory.text('Hello! How can I help you today?'));
116
+ }
117
+
118
+ return {
119
+ statusCode: 200,
120
+ type: 'application/vnd.microsoft.activity.message',
121
+ value: { message: 'Action processed' },
122
+ };
123
+ }
124
+
125
+ // Helper to remove @mention from message text
126
+ private removeMentionText(context: TurnContext): string {
127
+ let text = context.activity.text || '';
128
+ const mentions = context.activity.entities?.filter(
129
+ (e) => e.type === 'mention'
130
+ );
131
+
132
+ if (mentions && mentions.length > 0) {
133
+ for (const mention of mentions) {
134
+ if (mention.mentioned?.id === context.activity.recipient.id) {
135
+ text = text.replace(mention.text || '', '').trim();
136
+ }
137
+ }
138
+ }
139
+
140
+ return text;
141
+ }
142
+ }
@@ -0,0 +1,134 @@
1
+ import { CardFactory, Attachment } from 'botbuilder';
2
+
3
+ export function createWelcomeCard(userName: string): Attachment {
4
+ const card = {
5
+ type: 'AdaptiveCard',
6
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
7
+ version: '1.4',
8
+ body: [
9
+ {
10
+ type: 'TextBlock',
11
+ size: 'Large',
12
+ weight: 'Bolder',
13
+ text: `Welcome, ${userName}!`,
14
+ wrap: true,
15
+ },
16
+ {
17
+ type: 'TextBlock',
18
+ text: "I'm a Teams bot built with Bot Framework. Here's what I can do:",
19
+ wrap: true,
20
+ spacing: 'Medium',
21
+ },
22
+ {
23
+ type: 'FactSet',
24
+ facts: [
25
+ { title: 'Echo', value: 'I repeat what you say' },
26
+ { title: 'Help', value: 'Type "help" for commands' },
27
+ { title: 'About', value: 'Type "about" for info' },
28
+ ],
29
+ },
30
+ {
31
+ type: 'TextBlock',
32
+ text: 'Try sending me a message!',
33
+ wrap: true,
34
+ spacing: 'Medium',
35
+ color: 'Accent',
36
+ },
37
+ ],
38
+ actions: [
39
+ {
40
+ type: 'Action.Submit',
41
+ title: 'Say Hello',
42
+ data: { action: 'hello' },
43
+ },
44
+ ],
45
+ };
46
+
47
+ return CardFactory.adaptiveCard(card);
48
+ }
49
+
50
+ export function createHelpCard(): Attachment {
51
+ const card = {
52
+ type: 'AdaptiveCard',
53
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
54
+ version: '1.4',
55
+ body: [
56
+ {
57
+ type: 'TextBlock',
58
+ size: 'Medium',
59
+ weight: 'Bolder',
60
+ text: 'Available Commands',
61
+ wrap: true,
62
+ },
63
+ {
64
+ type: 'Container',
65
+ items: [
66
+ {
67
+ type: 'TextBlock',
68
+ text: '**help** - Show this help message',
69
+ wrap: true,
70
+ },
71
+ {
72
+ type: 'TextBlock',
73
+ text: '**about** - Learn about this bot',
74
+ wrap: true,
75
+ },
76
+ {
77
+ type: 'TextBlock',
78
+ text: '**echo [message]** - Echo your message',
79
+ wrap: true,
80
+ },
81
+ {
82
+ type: 'TextBlock',
83
+ text: 'Or just type anything and I will echo it back!',
84
+ wrap: true,
85
+ spacing: 'Medium',
86
+ isSubtle: true,
87
+ },
88
+ ],
89
+ },
90
+ ],
91
+ };
92
+
93
+ return CardFactory.adaptiveCard(card);
94
+ }
95
+
96
+ export function createAboutCard(): Attachment {
97
+ const card = {
98
+ type: 'AdaptiveCard',
99
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
100
+ version: '1.4',
101
+ body: [
102
+ {
103
+ type: 'TextBlock',
104
+ size: 'Medium',
105
+ weight: 'Bolder',
106
+ text: 'About This Bot',
107
+ wrap: true,
108
+ },
109
+ {
110
+ type: 'FactSet',
111
+ facts: [
112
+ { title: 'Name', value: '{{projectName}}' },
113
+ { title: 'Framework', value: 'Bot Framework SDK v4' },
114
+ { title: 'Scaffold', value: 'create-dynamics-app (teams-bot)' },
115
+ ],
116
+ },
117
+ {
118
+ type: 'TextBlock',
119
+ text: 'This bot demonstrates the fundamentals of building a Teams bot using the Bot Framework SDK.',
120
+ wrap: true,
121
+ spacing: 'Medium',
122
+ },
123
+ ],
124
+ actions: [
125
+ {
126
+ type: 'Action.OpenUrl',
127
+ title: 'View Documentation',
128
+ url: 'https://learn.microsoft.com/en-us/microsoftteams/platform/bots/what-are-bots',
129
+ },
130
+ ],
131
+ };
132
+
133
+ return CardFactory.adaptiveCard(card);
134
+ }
@@ -0,0 +1,59 @@
1
+ import express, { Request, Response } from 'express';
2
+ import { config, validateConfig } from './config';
3
+ import { createBotAdapter } from './adapters/botAdapter';
4
+ import { TeamsBot } from './bot/TeamsBot';
5
+ import { startInteractiveTest } from './mock/mockMode';
6
+
7
+ validateConfig();
8
+
9
+ // Interactive mock mode: console chat loop, no server / no Azure.
10
+ if (process.argv.includes('--interactive')) {
11
+ startInteractiveTest();
12
+ } else {
13
+ const app = express();
14
+ app.use(express.json());
15
+
16
+ const adapter = createBotAdapter();
17
+ const bot = new TeamsBot();
18
+
19
+ // Health check endpoint (the deploy pipeline gates on this)
20
+ app.get('/health', (_req: Request, res: Response) => {
21
+ res.json({
22
+ status: 'healthy',
23
+ timestamp: new Date().toISOString(),
24
+ mockMode: config.mockMode,
25
+ });
26
+ });
27
+
28
+ // Bot messaging endpoint
29
+ app.post('/api/messages', async (req: Request, res: Response) => {
30
+ try {
31
+ await adapter.process(req, res, (context) => bot.run(context));
32
+ } catch (error) {
33
+ console.error('Error processing message:', error);
34
+ res.status(500).send('Internal Server Error');
35
+ }
36
+ });
37
+
38
+ app.get('/', (_req: Request, res: Response) => {
39
+ res.json({
40
+ name: '{{projectName}}',
41
+ description: 'Microsoft Teams echo bot (Bot Framework SDK v4)',
42
+ endpoints: { messaging: '/api/messages', health: '/health' },
43
+ mockMode: config.mockMode,
44
+ });
45
+ });
46
+
47
+ app.listen(config.port, () => {
48
+ console.log(`\nBot server is running!`);
49
+ console.log(` Local: http://localhost:${config.port}`);
50
+ console.log(` Health: http://localhost:${config.port}/health`);
51
+ console.log(` Mode: ${config.mockMode ? 'Mock (development)' : 'Production'}`);
52
+ console.log(`\n Messaging endpoint: /api/messages`);
53
+
54
+ if (config.mockMode) {
55
+ console.log(`\nTip: run with --interactive for console testing:`);
56
+ console.log(` npm run dev -- --interactive`);
57
+ }
58
+ });
59
+ }