fraim-framework 2.0.35 → 2.0.36

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 (51) hide show
  1. package/bin/fraim.js +52 -5
  2. package/dist/registry/scripts/cleanup-branch.js +62 -33
  3. package/dist/registry/scripts/generate-engagement-emails.js +119 -44
  4. package/dist/registry/scripts/newsletter-helpers.js +208 -268
  5. package/dist/registry/scripts/profile-server.js +387 -0
  6. package/dist/tests/test-chalk-regression.js +18 -2
  7. package/dist/tests/test-client-scripts-validation.js +133 -0
  8. package/dist/tests/test-prep-issue.js +1 -34
  9. package/dist/tests/test-script-location-independence.js +76 -28
  10. package/package.json +2 -2
  11. package/registry/agent-guardrails.md +62 -62
  12. package/registry/rules/communication.md +121 -121
  13. package/registry/rules/continuous-learning.md +54 -54
  14. package/registry/rules/hitl-ppe-record-analysis.md +302 -302
  15. package/registry/rules/software-development-lifecycle.md +104 -104
  16. package/registry/scripts/cleanup-branch.ts +341 -0
  17. package/registry/scripts/code-quality-check.sh +559 -559
  18. package/registry/scripts/detect-tautological-tests.sh +38 -38
  19. package/registry/scripts/generate-engagement-emails.ts +830 -0
  20. package/registry/scripts/markdown-to-pdf.js +7 -3
  21. package/registry/scripts/newsletter-helpers.ts +777 -0
  22. package/registry/scripts/prep-issue.sh +30 -61
  23. package/registry/scripts/profile-server.ts +424 -0
  24. package/registry/scripts/run-thank-you-workflow.ts +122 -0
  25. package/registry/scripts/send-newsletter-simple.ts +102 -0
  26. package/registry/scripts/send-thank-you-emails.ts +57 -0
  27. package/registry/scripts/validate-openapi-limits.ts +366 -366
  28. package/registry/scripts/validate-test-coverage.ts +280 -280
  29. package/registry/scripts/verify-pr-comments.sh +70 -70
  30. package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -53
  31. package/registry/templates/evidence/Implementation-BugEvidence.md +85 -85
  32. package/registry/templates/evidence/Implementation-FeatureEvidence.md +120 -120
  33. package/registry/workflows/customer-development/insight-analysis.md +156 -156
  34. package/registry/workflows/customer-development/interview-preparation.md +421 -421
  35. package/registry/workflows/customer-development/strategic-brainstorming.md +146 -146
  36. package/registry/workflows/quality-assurance/iterative-improvement-cycle.md +562 -562
  37. package/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +669 -669
  38. package/dist/registry/scripts/build-scripts-generator.js +0 -205
  39. package/dist/registry/scripts/fraim-config.js +0 -61
  40. package/dist/registry/scripts/generic-issues-api.js +0 -100
  41. package/dist/registry/scripts/openapi-generator.js +0 -664
  42. package/dist/registry/scripts/performance/profile-server.js +0 -390
  43. package/dist/test-utils.js +0 -96
  44. package/dist/tests/esm-compat.js +0 -11
  45. package/dist/tests/test-chalk-esm-issue.js +0 -159
  46. package/dist/tests/test-chalk-real-world.js +0 -265
  47. package/dist/tests/test-chalk-resolution-issue.js +0 -304
  48. package/dist/tests/test-fraim-install-chalk-issue.js +0 -254
  49. package/dist/tests/test-npm-resolution-diagnostic.js +0 -140
  50. package/registry/templates/marketing/STORYTELLING-TEMPLATE.md +0 -130
  51. package/registry/workflows/marketing/storytelling.md +0 -65
@@ -134,7 +134,6 @@ try {
134
134
 
135
135
  // Support both 'repository' (new) and 'git' (legacy/current) schemas
136
136
  let repo = config.repository;
137
- let defaultBranch = 'master'; // Default fallback
138
137
 
139
138
  if (!repo) {
140
139
  if (config.git) {
@@ -143,18 +142,13 @@ try {
143
142
  name: config.git.repoName,
144
143
  url: config.git.repoUrl || \`https://github.com/\${config.git.repoOwner}/\${config.git.repoName}.git\`
145
144
  };
146
- // Extract defaultBranch from git config
147
- defaultBranch = config.git.defaultBranch || 'master';
148
145
  }
149
- } else {
150
- // Extract defaultBranch from repository config
151
- defaultBranch = repo.defaultBranch || 'master';
152
146
  }
153
147
 
154
148
  if (!repo || !repo.owner || !repo.name || !repo.url) {
155
149
  process.exit(1);
156
150
  }
157
- console.log(\`\${repo.owner}|\${repo.name}|\${repo.url}|\${defaultBranch}\`);
151
+ console.log(\`\${repo.owner}:\${repo.name}:\${repo.url}\`);
158
152
  } catch (e) {
159
153
  process.exit(1);
160
154
  }
@@ -167,14 +161,13 @@ if [ $? -ne 0 ]; then
167
161
  exit 1
168
162
  fi
169
163
 
170
- # Split the result into variables using pipe delimiter
171
- IFS='|' read -r REPO_OWNER REPO_NAME REPO_URL CONFIG_DEFAULT_BRANCH <<< "$REPO_INFO"
164
+ # Split the result into variables
165
+ IFS=':' read -r REPO_OWNER REPO_NAME REPO_URL <<< "$REPO_INFO"
172
166
 
173
167
  echo "Repository Configuration:"
174
168
  echo " Owner: $REPO_OWNER"
175
169
  echo " Name: $REPO_NAME"
176
170
  echo " URL: $REPO_URL"
177
- echo " Default Branch: $CONFIG_DEFAULT_BRANCH"
178
171
  echo
179
172
 
180
173
  echo "=== $REPO_NAME - Issue Preparation ==="
@@ -202,69 +195,45 @@ echo "Step 1: Determining base branch from current repository..."
202
195
 
203
196
  # Determine base branch from the ORIGINAL repository (before cloning)
204
197
  if [ "$USE_DEFAULT_BRANCH" = true ]; then
205
- # Use the default branch from FRAIM config
206
- DEFAULT_BRANCH="$CONFIG_DEFAULT_BRANCH"
207
- echo "✓ Using default branch from FRAIM config: $DEFAULT_BRANCH"
198
+ # Detect the default branch
199
+ DEFAULT_BRANCH=""
200
+ for candidate in main master develop; do
201
+ if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
202
+ DEFAULT_BRANCH="$candidate"
203
+ echo "✓ Detected default branch: $DEFAULT_BRANCH"
204
+ break
205
+ fi
206
+ done
208
207
 
209
- # Verify the branch exists on remote
210
- if git ls-remote --exit-code --heads "$REPO_URL" "$DEFAULT_BRANCH" >/dev/null 2>&1; then
211
- echo "✓ Confirmed branch '$DEFAULT_BRANCH' exists on remote"
212
- else
213
- echo "⚠️ Warning: Configured default branch '$DEFAULT_BRANCH' not found on remote"
214
- echo " Falling back to branch detection..."
208
+ # Fallback to master if nothing else worked
209
+ if [ -z "$DEFAULT_BRANCH" ]; then
210
+ DEFAULT_BRANCH="master"
211
+ echo "⚠️ Could not detect default branch, defaulting to: $DEFAULT_BRANCH"
212
+ fi
213
+
214
+ BASE_BRANCH="$DEFAULT_BRANCH"
215
+ echo "Using detected default branch as base: $BASE_BRANCH (--use-default flag)"
216
+ else
217
+ # Get current branch from the original repo
218
+ ORIGINAL_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
219
+
220
+ if [ -z "$ORIGINAL_BRANCH" ]; then
221
+ echo "Warning: Detached HEAD detected in original repo, falling back to default branch detection"
215
222
 
216
- # Fallback to detection if configured branch doesn't exist
223
+ # Detect the default branch
217
224
  DEFAULT_BRANCH=""
218
225
  for candidate in main master develop; do
219
226
  if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
220
227
  DEFAULT_BRANCH="$candidate"
221
- echo "✓ Detected fallback branch: $DEFAULT_BRANCH"
228
+ echo "✓ Detected default branch: $DEFAULT_BRANCH"
222
229
  break
223
230
  fi
224
231
  done
225
232
 
226
- # Final fallback to master
233
+ # Fallback to master if nothing else worked
227
234
  if [ -z "$DEFAULT_BRANCH" ]; then
228
235
  DEFAULT_BRANCH="master"
229
- echo "⚠️ Could not detect any branch, defaulting to: $DEFAULT_BRANCH"
230
- fi
231
- fi
232
-
233
- BASE_BRANCH="$DEFAULT_BRANCH"
234
- echo "Using default branch as base: $BASE_BRANCH (--use-default flag)"
235
- else
236
- # Get current branch from the original repo
237
- ORIGINAL_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
238
-
239
- if [ -z "$ORIGINAL_BRANCH" ]; then
240
- echo "Warning: Detached HEAD detected in original repo, falling back to configured default branch"
241
-
242
- # Use the default branch from FRAIM config
243
- DEFAULT_BRANCH="$CONFIG_DEFAULT_BRANCH"
244
- echo "✓ Using default branch from FRAIM config: $DEFAULT_BRANCH"
245
-
246
- # Verify the branch exists on remote
247
- if git ls-remote --exit-code --heads "$REPO_URL" "$DEFAULT_BRANCH" >/dev/null 2>&1; then
248
- echo "✓ Confirmed branch '$DEFAULT_BRANCH' exists on remote"
249
- else
250
- echo "⚠️ Warning: Configured default branch '$DEFAULT_BRANCH' not found on remote"
251
- echo " Falling back to branch detection..."
252
-
253
- # Fallback to detection if configured branch doesn't exist
254
- DEFAULT_BRANCH=""
255
- for candidate in main master develop; do
256
- if git ls-remote --exit-code --heads "$REPO_URL" "$candidate" >/dev/null 2>&1; then
257
- DEFAULT_BRANCH="$candidate"
258
- echo "✓ Detected fallback branch: $DEFAULT_BRANCH"
259
- break
260
- fi
261
- done
262
-
263
- # Final fallback to master
264
- if [ -z "$DEFAULT_BRANCH" ]; then
265
- DEFAULT_BRANCH="master"
266
- echo "⚠️ Could not detect any branch, defaulting to: $DEFAULT_BRANCH"
267
- fi
236
+ echo "⚠️ Could not detect default branch, defaulting to: $DEFAULT_BRANCH"
268
237
  fi
269
238
 
270
239
  BASE_BRANCH="$DEFAULT_BRANCH"
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env npx tsx
2
+
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ /**
8
+ * Production Server Profiler
9
+ * Automates the analysis of Azure App Service performance for coding agents.
10
+ *
11
+ * Usage: npx tsx profile-server.ts [--prod|--preprod|--local] [--logs]
12
+ *
13
+ * Self-contained script - reads configuration from .fraim/config.json
14
+ * Requires: Azure CLI (az) installed and authenticated
15
+ */
16
+
17
+ // Self-contained configuration loading
18
+ interface FraimConfig {
19
+ persona: { name: string };
20
+ azure?: {
21
+ prodAppName?: string;
22
+ prodResourceGroup?: string;
23
+ preprodAppName?: string;
24
+ preprodResourceGroup?: string;
25
+ localAppName?: string;
26
+ localResourceGroup?: string;
27
+ };
28
+ }
29
+
30
+ function loadClientConfig(): FraimConfig {
31
+ const configPath = join(process.cwd(), '.fraim', 'config.json');
32
+ if (!existsSync(configPath)) {
33
+ throw new Error('.fraim/config.json not found. Run fraim init first.');
34
+ }
35
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
36
+ }
37
+
38
+ function getEnvOr(keys: string[], fallback: string): string {
39
+ for (const key of keys) {
40
+ const value = process.env[key];
41
+ if (value && value.length) return value;
42
+ }
43
+ return fallback;
44
+ }
45
+
46
+ interface ProfilingConfig {
47
+ env: 'prod' | 'preprod' | 'local';
48
+ appName: string;
49
+ resourceGroup: string;
50
+ }
51
+
52
+ function checkAzureCLI(): boolean {
53
+ try {
54
+ execSync('az --version', { stdio: 'pipe' });
55
+ return true;
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ function checkAzureAuth(): boolean {
62
+ try {
63
+ execSync('az account show', { stdio: 'pipe' });
64
+ return true;
65
+ } catch (error) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async function getAppServicePlanMetrics(appName: string, resourceGroup: string): Promise<void> {
71
+ try {
72
+ console.log('\n--- ☁️ App Service Plan Metrics ---');
73
+
74
+ // Get App Service Plan ID
75
+ const planIdCmd = `az webapp show --name ${appName} --resource-group ${resourceGroup} --query "appServicePlanId" -o tsv`;
76
+ const planId = execSync(planIdCmd, { encoding: 'utf8' }).trim();
77
+ const planName = planId.split('/').pop();
78
+ console.log(`📋 Plan: ${planName}`);
79
+
80
+ // Get CPU and Memory metrics for last 5 minutes
81
+ console.log('📊 Fetching CPU and Memory metrics (last 5 minutes)...');
82
+ const metricsCmd = `az monitor metrics list --resource "${planId}" --metrics CpuPercentage MemoryPercentage --interval PT1M --query "value[].{name:name.value, data:timeseries[0].data}" -o json`;
83
+
84
+ const metricsOutput = execSync(metricsCmd, { encoding: 'utf8' });
85
+ const metrics = JSON.parse(metricsOutput);
86
+
87
+ for (const metric of metrics) {
88
+ const latestData = metric.data[metric.data.length - 1];
89
+ const value = latestData?.average || latestData?.total || 'N/A';
90
+ console.log(` ${metric.name}: ${typeof value === 'number' ? value.toFixed(2) + '%' : value}`);
91
+ }
92
+ } catch (error: any) {
93
+ console.log(`⚠️ Could not fetch App Service Plan metrics: ${error.message.split('\n')[0]}`);
94
+ }
95
+ }
96
+
97
+ async function getAppServiceMetrics(appName: string, resourceGroup: string): Promise<void> {
98
+ try {
99
+ console.log('\n--- 🖥️ App Service Metrics ---');
100
+
101
+ // Get subscription ID
102
+ const subscriptionId = execSync('az account show --query id -o tsv', { encoding: 'utf8' }).trim();
103
+
104
+ // Get app-level metrics using Azure CLI
105
+ console.log('📊 Fetching App Service metrics (last 5 minutes)...');
106
+ const appResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${appName}`;
107
+
108
+ const appMetricsCmd = `az monitor metrics list --resource "${appResourceId}" --metrics CpuTime Requests Http2xx Http4xx Http5xx --interval PT1M --query "value[].{name:name.value, data:timeseries[0].data}" -o json`;
109
+
110
+ const appMetricsOutput = execSync(appMetricsCmd, { encoding: 'utf8' });
111
+ const appMetrics = JSON.parse(appMetricsOutput);
112
+
113
+ for (const metric of appMetrics) {
114
+ const latestData = metric.data[metric.data.length - 1];
115
+ const value = latestData?.total || latestData?.average || 'N/A';
116
+ let displayValue = value;
117
+
118
+ if (typeof value === 'number') {
119
+ if (metric.name === 'CpuTime') {
120
+ displayValue = `${value.toFixed(2)}s`;
121
+ } else {
122
+ displayValue = value.toString();
123
+ }
124
+ }
125
+
126
+ console.log(` ${metric.name}: ${displayValue}`);
127
+ }
128
+ } catch (error: any) {
129
+ console.log(`⚠️ Could not fetch App Service metrics: ${error.message.split('\n')[0]}`);
130
+ }
131
+ }
132
+
133
+ async function getProcessInformation(appName: string): Promise<void> {
134
+ try {
135
+ console.log('\n--- ⚙️ Process Information (Kudu API) ---');
136
+
137
+ // Get access token for Kudu API
138
+ const token = execSync('az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv', { encoding: 'utf8' }).trim();
139
+
140
+ // Import axios dynamically
141
+ const axios = (await import('axios')).default;
142
+
143
+ // Get process list from Kudu
144
+ console.log('📋 Fetching running processes...');
145
+ const processResponse = await axios.get(`https://${appName}.scm.azurewebsites.net/api/processes`, {
146
+ headers: { Authorization: `Bearer ${token}` },
147
+ timeout: 10000
148
+ });
149
+
150
+ const processes = processResponse.data;
151
+
152
+ // Sort by CPU usage and show top processes
153
+ const sortedProcesses = processes
154
+ .filter((p: any) => p.cpu_usage !== undefined)
155
+ .sort((a: any, b: any) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
156
+ .slice(0, 10);
157
+
158
+ console.log('\n🔥 Top 10 Processes by CPU Usage:');
159
+ console.log('PID\tCPU%\tMemory(MB)\tName');
160
+ console.log('---\t----\t---------\t----');
161
+
162
+ for (const process of sortedProcesses) {
163
+ const pid = process.id || 'N/A';
164
+ const cpu = process.cpu_usage ? process.cpu_usage.toFixed(2) : '0.00';
165
+ const memory = process.working_set ? (process.working_set / 1024 / 1024).toFixed(1) : 'N/A';
166
+ const name = process.name || 'Unknown';
167
+
168
+ console.log(`${pid}\t${cpu}\t${memory}\t\t${name.substring(0, 30)}`);
169
+ }
170
+
171
+ } catch (error: any) {
172
+ console.log(`⚠️ Could not fetch process information: ${error.message.split('\n')[0]}`);
173
+ console.log(' This might be due to authentication issues or Kudu API being unavailable.');
174
+ }
175
+ }
176
+
177
+ async function getSystemInformation(appName: string): Promise<void> {
178
+ try {
179
+ console.log('\n--- 🖥️ System Information ---');
180
+
181
+ const token = execSync('az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv', { encoding: 'utf8' }).trim();
182
+ const axios = (await import('axios')).default;
183
+
184
+ // Get system info using Kudu command API
185
+ console.log('📊 Fetching system information...');
186
+
187
+ const commands = [
188
+ { name: 'Memory Info', cmd: 'cat /proc/meminfo | head -10' },
189
+ { name: 'CPU Info', cmd: 'cat /proc/cpuinfo | grep "model name" | head -1' },
190
+ { name: 'Disk Usage', cmd: 'df -h | head -5' },
191
+ { name: 'Load Average', cmd: 'uptime' }
192
+ ];
193
+
194
+ for (const command of commands) {
195
+ try {
196
+ const response = await axios.post(`https://${appName}.scm.azurewebsites.net/api/command`, {
197
+ command: command.cmd,
198
+ dir: '/home'
199
+ }, {
200
+ headers: { Authorization: `Bearer ${token}` },
201
+ timeout: 5000
202
+ });
203
+
204
+ console.log(`\n${command.name}:`);
205
+ const output = response.data.Output || response.data.Error || 'No output';
206
+ console.log(output.split('\n').map((line: string) => ` ${line}`).join('\n'));
207
+
208
+ } catch (cmdError: any) {
209
+ console.log(`\n${command.name}: Could not retrieve (${cmdError.message.split('\n')[0]})`);
210
+ }
211
+ }
212
+
213
+ } catch (error: any) {
214
+ console.log(`⚠️ Could not fetch system information: ${error.message.split('\n')[0]}`);
215
+ }
216
+ }
217
+
218
+ async function getApplicationLogs(appName: string, resourceGroup: string): Promise<void> {
219
+ try {
220
+ console.log('\n--- 📋 Recent Application Logs ---');
221
+
222
+ // Get recent logs using Azure CLI
223
+ console.log('📄 Fetching recent application logs...');
224
+ const logsCmd = `az webapp log tail --name ${appName} --resource-group ${resourceGroup} --provider application --timeout 5`;
225
+
226
+ try {
227
+ const logs = execSync(logsCmd, { encoding: 'utf8', timeout: 10000 });
228
+ console.log('Recent logs:');
229
+ console.log(logs.split('\n').slice(-20).map(line => ` ${line}`).join('\n'));
230
+ } catch (logError) {
231
+ console.log('⚠️ Could not fetch logs - they may not be enabled or available');
232
+ console.log(' Enable application logging in Azure portal for better diagnostics');
233
+ }
234
+
235
+ } catch (error: any) {
236
+ console.log(`⚠️ Could not fetch application logs: ${error.message.split('\n')[0]}`);
237
+ }
238
+ }
239
+
240
+ async function profileLocalEnvironment(): Promise<void> {
241
+ console.log('\n--- 🏠 Local Environment Profiling ---');
242
+
243
+ try {
244
+ // Basic system information
245
+ console.log('💻 System Information:');
246
+
247
+ if (process.platform === 'win32') {
248
+ try {
249
+ const systemInfo = execSync('systeminfo | findstr /C:"Total Physical Memory" /C:"Available Physical Memory" /C:"Processor"', { encoding: 'utf8' });
250
+ console.log(systemInfo.split('\n').map(line => ` ${line.trim()}`).filter(line => line).join('\n'));
251
+ } catch {
252
+ console.log(' Could not retrieve Windows system info');
253
+ }
254
+ } else {
255
+ try {
256
+ const memInfo = execSync('free -h', { encoding: 'utf8' });
257
+ const cpuInfo = execSync('cat /proc/cpuinfo | grep "model name" | head -1', { encoding: 'utf8' });
258
+ console.log(' Memory:');
259
+ console.log(memInfo.split('\n').map(line => ` ${line}`).join('\n'));
260
+ console.log(' CPU:');
261
+ console.log(` ${cpuInfo.trim()}`);
262
+ } catch {
263
+ console.log(' Could not retrieve Linux system info');
264
+ }
265
+ }
266
+
267
+ // Node.js process information
268
+ console.log('\n🟢 Node.js Process Information:');
269
+ console.log(` Node Version: ${process.version}`);
270
+ console.log(` Platform: ${process.platform}`);
271
+ console.log(` Architecture: ${process.arch}`);
272
+ console.log(` Memory Usage:`);
273
+ const memUsage = process.memoryUsage();
274
+ console.log(` RSS: ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`);
275
+ console.log(` Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
276
+ console.log(` Heap Total: ${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`);
277
+ console.log(` External: ${(memUsage.external / 1024 / 1024).toFixed(2)} MB`);
278
+
279
+ } catch (error: any) {
280
+ console.log(`⚠️ Error profiling local environment: ${error.message}`);
281
+ }
282
+ }
283
+
284
+ async function main() {
285
+ const config = loadClientConfig();
286
+
287
+ // Azure configuration with environment variable fallbacks
288
+ const azureConfig = {
289
+ prodAppName: config.azure?.prodAppName || getEnvOr(['FRAIM_AZURE_PROD_APP_NAME'], 'fraim-app-prod'),
290
+ prodResourceGroup: config.azure?.prodResourceGroup || getEnvOr(['FRAIM_AZURE_PROD_RESOURCE_GROUP'], 'fraim-prod-rg'),
291
+ preprodAppName: config.azure?.preprodAppName || getEnvOr(['FRAIM_AZURE_PREPROD_APP_NAME'], 'fraim-app-pre-prod'),
292
+ preprodResourceGroup: config.azure?.preprodResourceGroup || getEnvOr(['FRAIM_AZURE_PREPROD_RESOURCE_GROUP'], 'fraim-pre-prod-rg'),
293
+ localAppName: config.azure?.localAppName || getEnvOr(['FRAIM_AZURE_LOCAL_APP_NAME'], 'local'),
294
+ localResourceGroup: config.azure?.localResourceGroup || getEnvOr(['FRAIM_AZURE_LOCAL_RESOURCE_GROUP'], 'local')
295
+ };
296
+
297
+ const CONFIGS: Record<string, ProfilingConfig> = {
298
+ prod: {
299
+ env: 'prod',
300
+ appName: azureConfig.prodAppName,
301
+ resourceGroup: azureConfig.prodResourceGroup
302
+ },
303
+ preprod: {
304
+ env: 'preprod',
305
+ appName: azureConfig.preprodAppName,
306
+ resourceGroup: azureConfig.preprodResourceGroup
307
+ },
308
+ local: {
309
+ env: 'local',
310
+ appName: azureConfig.localAppName,
311
+ resourceGroup: azureConfig.localResourceGroup
312
+ }
313
+ };
314
+
315
+ const args = process.argv.slice(2);
316
+ let profileConfig: ProfilingConfig = CONFIGS.local;
317
+ const includeLogs = args.includes('--logs');
318
+
319
+ if (args.includes('--help') || args.includes('-h')) {
320
+ console.log(`
321
+ 🔍 Azure App Service Profiler
322
+
323
+ Usage:
324
+ npx tsx profile-server.ts [options]
325
+
326
+ Options:
327
+ --prod Profile production environment
328
+ --preprod Profile pre-production environment
329
+ --local Profile local environment (default)
330
+ --logs Include application logs in analysis
331
+ --help, -h Show this help message
332
+
333
+ Examples:
334
+ # Profile production server
335
+ npx tsx profile-server.ts --prod
336
+
337
+ # Profile with logs
338
+ npx tsx profile-server.ts --prod --logs
339
+
340
+ # Profile local environment
341
+ npx tsx profile-server.ts --local
342
+
343
+ Requirements:
344
+ - Azure CLI (az) installed and authenticated
345
+ - Proper permissions to access App Service resources
346
+ - For Kudu API access: Contributor role on the App Service
347
+
348
+ Configuration:
349
+ Reads Azure settings from .fraim/config.json or environment variables:
350
+ - FRAIM_AZURE_PROD_APP_NAME
351
+ - FRAIM_AZURE_PROD_RESOURCE_GROUP
352
+ - FRAIM_AZURE_PREPROD_APP_NAME
353
+ - FRAIM_AZURE_PREPROD_RESOURCE_GROUP
354
+ `);
355
+ return;
356
+ }
357
+
358
+ if (args.includes('--prod')) profileConfig = CONFIGS.prod;
359
+ else if (args.includes('--preprod')) profileConfig = CONFIGS.preprod;
360
+ else if (args.includes('--local')) profileConfig = CONFIGS.local;
361
+
362
+ console.log(`\n🚀 Starting Server Profiling`);
363
+ console.log(`📊 Environment: ${profileConfig.env.toUpperCase()}`);
364
+ console.log(`🏷️ App: ${profileConfig.appName}`);
365
+ console.log(`📁 Resource Group: ${profileConfig.resourceGroup}`);
366
+
367
+ if (profileConfig.env === 'local') {
368
+ await profileLocalEnvironment();
369
+ console.log('\n✅ Local profiling completed');
370
+ return;
371
+ }
372
+
373
+ // Check prerequisites for Azure profiling
374
+ console.log('\n🔍 Checking prerequisites...');
375
+
376
+ if (!checkAzureCLI()) {
377
+ console.error('❌ Azure CLI not found. Please install Azure CLI first.');
378
+ console.error(' Download from: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli');
379
+ process.exit(1);
380
+ }
381
+ console.log('✅ Azure CLI found');
382
+
383
+ if (!checkAzureAuth()) {
384
+ console.error('❌ Not authenticated with Azure. Please run: az login');
385
+ process.exit(1);
386
+ }
387
+ console.log('✅ Azure authentication verified');
388
+
389
+ try {
390
+ // Verify app exists
391
+ console.log(`\n🔍 Verifying App Service: ${profileConfig.appName}`);
392
+ execSync(`az webapp show --name ${profileConfig.appName} --resource-group ${profileConfig.resourceGroup} --query name -o tsv`, { stdio: 'pipe' });
393
+ console.log('✅ App Service found');
394
+
395
+ // Run profiling analysis
396
+ await getAppServicePlanMetrics(profileConfig.appName, profileConfig.resourceGroup);
397
+ await getAppServiceMetrics(profileConfig.appName, profileConfig.resourceGroup);
398
+ await getProcessInformation(profileConfig.appName);
399
+ await getSystemInformation(profileConfig.appName);
400
+
401
+ if (includeLogs) {
402
+ await getApplicationLogs(profileConfig.appName, profileConfig.resourceGroup);
403
+ }
404
+
405
+ console.log('\n✅ Server profiling completed successfully');
406
+ console.log('\n💡 Tips:');
407
+ console.log(' - Use --logs flag to include application logs');
408
+ console.log(' - High CPU usage may indicate performance bottlenecks');
409
+ console.log(' - High memory usage may indicate memory leaks');
410
+ console.log(' - Check process list for unexpected or resource-heavy processes');
411
+
412
+ } catch (error: any) {
413
+ console.error(`\n❌ Profiling failed: ${error.message}`);
414
+ console.error('\nTroubleshooting:');
415
+ console.error(' - Verify app name and resource group are correct');
416
+ console.error(' - Ensure you have proper permissions (Contributor role)');
417
+ console.error(' - Check if the App Service is running');
418
+ console.error(' - Try: az webapp list --query "[].{name:name, resourceGroup:resourceGroup}"');
419
+ process.exit(1);
420
+ }
421
+ }
422
+
423
+ // Run the main function
424
+ main().catch(console.error);
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * Runner script for thank-you email workflow
5
+ * Called by AI agents to generate thank-you emails
6
+ */
7
+
8
+ import { getResolvedIssues, findExecutiveByEmail, getPersonaEmailForExecutive } from './generate-engagement-emails';
9
+ import { writeFileSync, mkdirSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ interface CustomerData {
13
+ email: string;
14
+ name: string;
15
+ issues: Array<{
16
+ number: number;
17
+ title: string;
18
+ body: string;
19
+ }>;
20
+ executiveId?: string;
21
+ executiveName?: string;
22
+ personaEmail?: string;
23
+ }
24
+
25
+ async function main() {
26
+ console.log('🎯 Starting thank-you email generation workflow...\n');
27
+
28
+ // Step 1: Get resolved issues
29
+ console.log('📋 Step 1: Retrieving resolved issues...');
30
+ const issues = await getResolvedIssues();
31
+
32
+ console.log(`\n✅ Found ${issues.length} resolved issues\n`);
33
+
34
+ // Step 2: Extract customer information
35
+ console.log('📧 Step 2: Extracting customer information...\n');
36
+
37
+ const customerMap = new Map<string, CustomerData>();
38
+
39
+ for (const issue of issues) {
40
+ // Extract email and name from issue body or title
41
+ let customerEmail: string | null = null;
42
+ let customerName: string | null = null;
43
+
44
+ // Try to extract from body first
45
+ const bodyEmailMatch = issue.body?.match(/\*\*Reported by:\*\*\s+(.+?)\s+\((.+?)\)/);
46
+ if (bodyEmailMatch) {
47
+ customerName = bodyEmailMatch[1].trim();
48
+ customerEmail = bodyEmailMatch[2].trim();
49
+ }
50
+
51
+ // Try to extract from title (format: "email: summary")
52
+ if (!customerEmail) {
53
+ const titleEmailMatch = issue.title.match(/^([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}):/);
54
+ if (titleEmailMatch) {
55
+ customerEmail = titleEmailMatch[1];
56
+ }
57
+ }
58
+
59
+ if (!customerEmail) {
60
+ console.log(`⚠️ Issue #${issue.number}: Could not extract customer email, skipping`);
61
+ continue;
62
+ }
63
+
64
+ // Normalize email to lowercase for consistent matching
65
+ const normalizedEmail = customerEmail.toLowerCase();
66
+
67
+ // Add or update customer data
68
+ if (!customerMap.has(normalizedEmail)) {
69
+ customerMap.set(normalizedEmail, {
70
+ email: normalizedEmail,
71
+ name: customerName || 'Valued Customer',
72
+ issues: []
73
+ });
74
+ }
75
+
76
+ const customerData = customerMap.get(normalizedEmail)!;
77
+ customerData.issues.push({
78
+ number: issue.number,
79
+ title: issue.title,
80
+ body: issue.body || ''
81
+ });
82
+ }
83
+
84
+ console.log(`📊 Found ${customerMap.size} unique customers\n`);
85
+
86
+ // Step 3: Lookup executive data for each customer
87
+ console.log('🔍 Step 3: Looking up executive data...\n');
88
+
89
+ const entries = Array.from(customerMap.entries());
90
+ for (const [email, customerData] of entries) {
91
+ try {
92
+ const executive = await findExecutiveByEmail(email);
93
+
94
+ if (executive) {
95
+ customerData.executiveId = executive.id;
96
+ customerData.executiveName = executive.name;
97
+
98
+ if (executive.id) {
99
+ const personaEmail = await getPersonaEmailForExecutive(executive.id);
100
+ customerData.personaEmail = personaEmail;
101
+ console.log(`✅ ${email}: Found executive ${executive.name} (${executive.id}), Persona email: ${personaEmail}`);
102
+ } else {
103
+ console.log(`⚠️ ${email}: Found executive ${executive.name} but no ID`);
104
+ }
105
+ } else {
106
+ console.log(`⚠️ ${email}: No executive record found in database`);
107
+ }
108
+ } catch (error) {
109
+ console.error(`❌ ${email}: Error looking up executive:`, error);
110
+ }
111
+ }
112
+
113
+ console.log('\n📝 Customer Data Summary:\n');
114
+ console.log(JSON.stringify(Array.from(customerMap.values()), null, 2));
115
+
116
+ // Save to temp file for AI agent to process
117
+ const outputPath = join(process.cwd(), 'temp-customer-data.json');
118
+ writeFileSync(outputPath, JSON.stringify(Array.from(customerMap.values()), null, 2));
119
+ console.log(`\n💾 Customer data saved to: ${outputPath}`);
120
+ }
121
+
122
+ main().catch(console.error);