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.
- package/bin/fraim.js +52 -5
- package/dist/registry/scripts/cleanup-branch.js +62 -33
- package/dist/registry/scripts/generate-engagement-emails.js +119 -44
- package/dist/registry/scripts/newsletter-helpers.js +208 -268
- package/dist/registry/scripts/profile-server.js +387 -0
- package/dist/tests/test-chalk-regression.js +18 -2
- package/dist/tests/test-client-scripts-validation.js +133 -0
- package/dist/tests/test-prep-issue.js +1 -34
- package/dist/tests/test-script-location-independence.js +76 -28
- package/package.json +2 -2
- package/registry/agent-guardrails.md +62 -62
- package/registry/rules/communication.md +121 -121
- package/registry/rules/continuous-learning.md +54 -54
- package/registry/rules/hitl-ppe-record-analysis.md +302 -302
- package/registry/rules/software-development-lifecycle.md +104 -104
- package/registry/scripts/cleanup-branch.ts +341 -0
- package/registry/scripts/code-quality-check.sh +559 -559
- package/registry/scripts/detect-tautological-tests.sh +38 -38
- package/registry/scripts/generate-engagement-emails.ts +830 -0
- package/registry/scripts/markdown-to-pdf.js +7 -3
- package/registry/scripts/newsletter-helpers.ts +777 -0
- package/registry/scripts/prep-issue.sh +30 -61
- package/registry/scripts/profile-server.ts +424 -0
- package/registry/scripts/run-thank-you-workflow.ts +122 -0
- package/registry/scripts/send-newsletter-simple.ts +102 -0
- package/registry/scripts/send-thank-you-emails.ts +57 -0
- package/registry/scripts/validate-openapi-limits.ts +366 -366
- package/registry/scripts/validate-test-coverage.ts +280 -280
- package/registry/scripts/verify-pr-comments.sh +70 -70
- package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -53
- package/registry/templates/evidence/Implementation-BugEvidence.md +85 -85
- package/registry/templates/evidence/Implementation-FeatureEvidence.md +120 -120
- package/registry/workflows/customer-development/insight-analysis.md +156 -156
- package/registry/workflows/customer-development/interview-preparation.md +421 -421
- package/registry/workflows/customer-development/strategic-brainstorming.md +146 -146
- package/registry/workflows/quality-assurance/iterative-improvement-cycle.md +562 -562
- package/registry/workflows/reviewer/review-implementation-vs-feature-spec.md +669 -669
- package/dist/registry/scripts/build-scripts-generator.js +0 -205
- package/dist/registry/scripts/fraim-config.js +0 -61
- package/dist/registry/scripts/generic-issues-api.js +0 -100
- package/dist/registry/scripts/openapi-generator.js +0 -664
- package/dist/registry/scripts/performance/profile-server.js +0 -390
- package/dist/test-utils.js +0 -96
- package/dist/tests/esm-compat.js +0 -11
- package/dist/tests/test-chalk-esm-issue.js +0 -159
- package/dist/tests/test-chalk-real-world.js +0 -265
- package/dist/tests/test-chalk-resolution-issue.js +0 -304
- package/dist/tests/test-fraim-install-chalk-issue.js +0 -254
- package/dist/tests/test-npm-resolution-diagnostic.js +0 -140
- package/registry/templates/marketing/STORYTELLING-TEMPLATE.md +0 -130
- 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}
|
|
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
|
|
171
|
-
IFS='
|
|
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
|
-
#
|
|
206
|
-
DEFAULT_BRANCH="
|
|
207
|
-
|
|
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
|
-
#
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
#
|
|
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
|
|
228
|
+
echo "✓ Detected default branch: $DEFAULT_BRANCH"
|
|
222
229
|
break
|
|
223
230
|
fi
|
|
224
231
|
done
|
|
225
232
|
|
|
226
|
-
#
|
|
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
|
|
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);
|