agileflow 3.3.0 → 3.4.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.
- package/CHANGELOG.md +5 -0
- package/README.md +6 -6
- package/lib/skill-loader.js +0 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +81 -0
- package/scripts/claude-tmux.sh +113 -22
- package/scripts/claude-watchdog.sh +225 -0
- package/scripts/generators/agent-registry.js +14 -1
- package/scripts/generators/inject-babysit.js +22 -9
- package/scripts/generators/inject-help.js +19 -9
- package/scripts/lib/audit-cleanup.js +250 -0
- package/scripts/lib/audit-registry.js +248 -0
- package/scripts/lib/feature-catalog.js +3 -3
- package/scripts/lib/gate-enforcer.js +295 -0
- package/scripts/lib/model-profiles.js +98 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/skill-catalog.js +557 -0
- package/scripts/lib/skill-recommender.js +311 -0
- package/scripts/lib/tdd-phase-manager.js +455 -0
- package/scripts/lib/team-events.js +34 -3
- package/scripts/lib/tmux-group-colors.js +113 -0
- package/scripts/messaging-bridge.js +209 -1
- package/scripts/spawn-audit-sessions.js +549 -0
- package/scripts/team-manager.js +37 -16
- package/scripts/tmux-close-windows.sh +180 -0
- package/src/core/agents/ads-audit-budget.md +181 -0
- package/src/core/agents/ads-audit-compliance.md +169 -0
- package/src/core/agents/ads-audit-creative.md +164 -0
- package/src/core/agents/ads-audit-google.md +226 -0
- package/src/core/agents/ads-audit-meta.md +183 -0
- package/src/core/agents/ads-audit-tracking.md +197 -0
- package/src/core/agents/ads-consensus.md +322 -0
- package/src/core/agents/brainstorm-analyzer-features.md +169 -0
- package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
- package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
- package/src/core/agents/brainstorm-analyzer-market.md +147 -0
- package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
- package/src/core/agents/brainstorm-consensus.md +237 -0
- package/src/core/agents/completeness-consensus.md +5 -5
- package/src/core/agents/perf-consensus.md +2 -2
- package/src/core/agents/security-consensus.md +2 -2
- package/src/core/agents/seo-analyzer-content.md +167 -0
- package/src/core/agents/seo-analyzer-images.md +187 -0
- package/src/core/agents/seo-analyzer-performance.md +206 -0
- package/src/core/agents/seo-analyzer-schema.md +176 -0
- package/src/core/agents/seo-analyzer-sitemap.md +172 -0
- package/src/core/agents/seo-analyzer-technical.md +144 -0
- package/src/core/agents/seo-consensus.md +289 -0
- package/src/core/agents/test-consensus.md +2 -2
- package/src/core/commands/ads/audit.md +375 -0
- package/src/core/commands/ads/budget.md +97 -0
- package/src/core/commands/ads/competitor.md +112 -0
- package/src/core/commands/ads/creative.md +85 -0
- package/src/core/commands/ads/google.md +112 -0
- package/src/core/commands/ads/landing.md +119 -0
- package/src/core/commands/ads/linkedin.md +112 -0
- package/src/core/commands/ads/meta.md +91 -0
- package/src/core/commands/ads/microsoft.md +115 -0
- package/src/core/commands/ads/plan.md +321 -0
- package/src/core/commands/ads/tiktok.md +129 -0
- package/src/core/commands/ads/youtube.md +124 -0
- package/src/core/commands/ads.md +128 -0
- package/src/core/commands/babysit.md +249 -1284
- package/src/core/commands/{audit → code}/completeness.md +35 -25
- package/src/core/commands/{audit → code}/legal.md +26 -16
- package/src/core/commands/{audit → code}/logic.md +27 -16
- package/src/core/commands/{audit → code}/performance.md +30 -20
- package/src/core/commands/{audit → code}/security.md +32 -19
- package/src/core/commands/{audit → code}/test.md +30 -20
- package/src/core/commands/{discovery → ideate}/brief.md +12 -12
- package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
- package/src/core/commands/ideate/features.md +435 -0
- package/src/core/commands/seo/audit.md +373 -0
- package/src/core/commands/seo/competitor.md +174 -0
- package/src/core/commands/seo/content.md +107 -0
- package/src/core/commands/seo/geo.md +229 -0
- package/src/core/commands/seo/hreflang.md +140 -0
- package/src/core/commands/seo/images.md +96 -0
- package/src/core/commands/seo/page.md +198 -0
- package/src/core/commands/seo/plan.md +163 -0
- package/src/core/commands/seo/programmatic.md +131 -0
- package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
- package/src/core/commands/seo/references/eeat-framework.md +110 -0
- package/src/core/commands/seo/references/quality-gates.md +91 -0
- package/src/core/commands/seo/references/schema-types.md +102 -0
- package/src/core/commands/seo/schema.md +183 -0
- package/src/core/commands/seo/sitemap.md +97 -0
- package/src/core/commands/seo/technical.md +100 -0
- package/src/core/commands/seo.md +107 -0
- package/src/core/commands/skill/list.md +68 -212
- package/src/core/commands/skill/recommend.md +216 -0
- package/src/core/commands/tdd-next.md +238 -0
- package/src/core/commands/tdd.md +210 -0
- package/src/core/experts/_core-expertise.yaml +105 -0
- package/src/core/experts/analytics/expertise.yaml +5 -99
- package/src/core/experts/codebase-query/expertise.yaml +3 -72
- package/src/core/experts/compliance/expertise.yaml +6 -72
- package/src/core/experts/database/expertise.yaml +9 -52
- package/src/core/experts/documentation/expertise.yaml +7 -140
- package/src/core/experts/integrations/expertise.yaml +7 -127
- package/src/core/experts/mentor/expertise.yaml +8 -35
- package/src/core/experts/monitoring/expertise.yaml +7 -49
- package/src/core/experts/performance/expertise.yaml +1 -26
- package/src/core/experts/security/expertise.yaml +9 -34
- package/src/core/experts/ui/expertise.yaml +6 -36
- package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
- package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
- package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
- package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
- package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
- package/src/core/templates/agileflow-metadata.json +15 -1
- package/tools/cli/installers/ide/_base-ide.js +42 -5
- package/tools/cli/installers/ide/claude-code.js +3 -3
- package/tools/cli/lib/content-injector.js +160 -12
- package/tools/cli/lib/docs-setup.js +1 -1
- package/src/core/commands/skill/create.md +0 -698
- package/src/core/commands/skill/delete.md +0 -316
- package/src/core/commands/skill/edit.md +0 -359
- package/src/core/commands/skill/test.md +0 -394
- package/src/core/commands/skill/upgrade.md +0 -552
- package/src/core/templates/skill-template.md +0 -117
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skill-recommender.js
|
|
4
|
+
*
|
|
5
|
+
* Tech stack detector + recommendation engine for skills.
|
|
6
|
+
* Reads package.json (or other project files) to detect the framework,
|
|
7
|
+
* styling, testing, database, and language. Maps detected stack to
|
|
8
|
+
* curated skills from skills.sh.
|
|
9
|
+
*
|
|
10
|
+
* Follows the signal-detectors.js pattern (detector functions returning recommendations).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { getAllCuratedSkills } = require('./skill-catalog');
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Tech Stack Detection
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect project tech stack from package.json and project files.
|
|
25
|
+
* @param {string} projectRoot - Project root directory
|
|
26
|
+
* @returns {Object} Detected stack with categories
|
|
27
|
+
*/
|
|
28
|
+
function detectTechStack(projectRoot) {
|
|
29
|
+
const stack = {
|
|
30
|
+
frameworks: [],
|
|
31
|
+
styling: [],
|
|
32
|
+
testing: [],
|
|
33
|
+
databases: [],
|
|
34
|
+
languages: [],
|
|
35
|
+
devops: [],
|
|
36
|
+
security: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Read package.json
|
|
40
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
41
|
+
let pkg = null;
|
|
42
|
+
if (fs.existsSync(pkgPath)) {
|
|
43
|
+
try {
|
|
44
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore parse errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (pkg) {
|
|
51
|
+
const allDeps = {
|
|
52
|
+
...pkg.dependencies,
|
|
53
|
+
...pkg.devDependencies,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Framework detection
|
|
57
|
+
if (allDeps.next) stack.frameworks.push('next', 'react');
|
|
58
|
+
else if (allDeps.react) stack.frameworks.push('react');
|
|
59
|
+
if (allDeps.vue || allDeps['vue-router']) stack.frameworks.push('vue');
|
|
60
|
+
if (allDeps.svelte || allDeps['@sveltejs/kit']) stack.frameworks.push('svelte', 'sveltekit');
|
|
61
|
+
if (allDeps['@angular/core']) stack.frameworks.push('angular');
|
|
62
|
+
if (allDeps.express) stack.frameworks.push('express', 'node');
|
|
63
|
+
if (allDeps.fastify) stack.frameworks.push('fastify', 'node');
|
|
64
|
+
if (allDeps.hono) stack.frameworks.push('hono', 'node');
|
|
65
|
+
if (allDeps['react-native'] || allDeps.expo) stack.frameworks.push('react-native', 'mobile');
|
|
66
|
+
if (allDeps.nuxt) stack.frameworks.push('nuxt', 'vue');
|
|
67
|
+
if (allDeps.astro) stack.frameworks.push('astro');
|
|
68
|
+
if (allDeps.remix || allDeps['@remix-run/react']) stack.frameworks.push('remix', 'react');
|
|
69
|
+
|
|
70
|
+
// Styling detection
|
|
71
|
+
if (allDeps.tailwindcss) stack.styling.push('tailwind', 'tailwindcss');
|
|
72
|
+
if (allDeps['styled-components']) stack.styling.push('styled-components');
|
|
73
|
+
if (allDeps['@emotion/react']) stack.styling.push('emotion');
|
|
74
|
+
if (allDeps.sass) stack.styling.push('sass');
|
|
75
|
+
|
|
76
|
+
// Testing detection
|
|
77
|
+
if (allDeps.jest) stack.testing.push('jest');
|
|
78
|
+
if (allDeps.vitest) stack.testing.push('vitest', 'vite');
|
|
79
|
+
if (allDeps.playwright || allDeps['@playwright/test']) stack.testing.push('playwright', 'e2e');
|
|
80
|
+
if (allDeps.cypress) stack.testing.push('cypress', 'e2e');
|
|
81
|
+
if (allDeps.mocha) stack.testing.push('mocha');
|
|
82
|
+
|
|
83
|
+
// Database detection
|
|
84
|
+
if (allDeps.prisma || allDeps['@prisma/client']) stack.databases.push('prisma', 'orm');
|
|
85
|
+
if (allDeps['@supabase/supabase-js']) stack.databases.push('supabase', 'postgres');
|
|
86
|
+
if (allDeps.mongoose || allDeps.mongodb) stack.databases.push('mongodb', 'nosql');
|
|
87
|
+
if (allDeps.pg || allDeps.postgres) stack.databases.push('postgresql', 'postgres');
|
|
88
|
+
if (allDeps.redis || allDeps.ioredis) stack.databases.push('redis', 'cache');
|
|
89
|
+
if (allDeps.drizzle || allDeps['drizzle-orm']) stack.databases.push('drizzle', 'orm');
|
|
90
|
+
if (allDeps.knex) stack.databases.push('knex', 'sql');
|
|
91
|
+
if (allDeps.sequelize) stack.databases.push('sequelize', 'orm');
|
|
92
|
+
if (allDeps.typeorm) stack.databases.push('typeorm', 'orm');
|
|
93
|
+
|
|
94
|
+
// Language detection
|
|
95
|
+
if (allDeps.typescript) stack.languages.push('typescript', 'ts');
|
|
96
|
+
if (allDeps['@apollo/server'] || allDeps['@apollo/client'] || allDeps.graphql) {
|
|
97
|
+
stack.frameworks.push('graphql', 'apollo');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// DevOps detection
|
|
101
|
+
if (allDeps.docker || fs.existsSync(path.join(projectRoot, 'Dockerfile'))) {
|
|
102
|
+
stack.devops.push('docker', 'containers');
|
|
103
|
+
}
|
|
104
|
+
if (fs.existsSync(path.join(projectRoot, '.github', 'workflows'))) {
|
|
105
|
+
stack.devops.push('github-actions', 'ci');
|
|
106
|
+
}
|
|
107
|
+
if (allDeps['socket.io'] || allDeps.ws) stack.frameworks.push('websocket', 'real-time');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Python detection
|
|
111
|
+
const pyFiles = ['requirements.txt', 'pyproject.toml', 'setup.py', 'Pipfile'];
|
|
112
|
+
for (const f of pyFiles) {
|
|
113
|
+
if (fs.existsSync(path.join(projectRoot, f))) {
|
|
114
|
+
stack.languages.push('python');
|
|
115
|
+
|
|
116
|
+
// Check for specific Python frameworks
|
|
117
|
+
try {
|
|
118
|
+
const content = fs.readFileSync(path.join(projectRoot, f), 'utf8');
|
|
119
|
+
if (content.includes('fastapi')) stack.frameworks.push('fastapi', 'python');
|
|
120
|
+
if (content.includes('django')) stack.frameworks.push('django', 'python');
|
|
121
|
+
if (content.includes('flask')) stack.frameworks.push('flask', 'python');
|
|
122
|
+
if (content.includes('pytest')) stack.testing.push('pytest');
|
|
123
|
+
if (content.includes('pydantic')) stack.frameworks.push('pydantic');
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignore read errors
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Go detection
|
|
132
|
+
if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
|
|
133
|
+
stack.languages.push('go', 'golang');
|
|
134
|
+
stack.frameworks.push('go-backend');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// PHP detection
|
|
138
|
+
if (fs.existsSync(path.join(projectRoot, 'composer.json'))) {
|
|
139
|
+
stack.languages.push('php');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Docker/K8s detection
|
|
143
|
+
if (fs.existsSync(path.join(projectRoot, 'Dockerfile'))) {
|
|
144
|
+
stack.devops.push('docker');
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
fs.existsSync(path.join(projectRoot, 'k8s')) ||
|
|
148
|
+
fs.existsSync(path.join(projectRoot, 'kubernetes'))
|
|
149
|
+
) {
|
|
150
|
+
stack.devops.push('kubernetes', 'k8s');
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
fs.existsSync(path.join(projectRoot, 'terraform')) ||
|
|
154
|
+
fs.existsSync(path.join(projectRoot, 'main.tf'))
|
|
155
|
+
) {
|
|
156
|
+
stack.devops.push('terraform', 'iac');
|
|
157
|
+
}
|
|
158
|
+
if (fs.existsSync(path.join(projectRoot, 'vercel.json'))) {
|
|
159
|
+
stack.devops.push('vercel', 'deployment');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Deduplicate all arrays
|
|
163
|
+
for (const key of Object.keys(stack)) {
|
|
164
|
+
stack[key] = [...new Set(stack[key])];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return stack;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Recommendation Engine
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Score a skill against detected tech stack.
|
|
176
|
+
* @param {Object} skill - Curated skill entry
|
|
177
|
+
* @param {Object} stack - Detected tech stack
|
|
178
|
+
* @returns {number} Relevance score (0-100)
|
|
179
|
+
*/
|
|
180
|
+
function scoreSkill(skill, stack) {
|
|
181
|
+
const allTags = [
|
|
182
|
+
...stack.frameworks,
|
|
183
|
+
...stack.styling,
|
|
184
|
+
...stack.testing,
|
|
185
|
+
...stack.databases,
|
|
186
|
+
...stack.languages,
|
|
187
|
+
...stack.devops,
|
|
188
|
+
...stack.security,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
if (allTags.length === 0) return 0;
|
|
192
|
+
|
|
193
|
+
let matchCount = 0;
|
|
194
|
+
for (const tag of skill.tags) {
|
|
195
|
+
if (allTags.includes(tag)) {
|
|
196
|
+
matchCount++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (matchCount === 0) return 0;
|
|
201
|
+
|
|
202
|
+
// Score: percentage of skill tags that match, weighted by total matches
|
|
203
|
+
const tagCoverage = matchCount / skill.tags.length;
|
|
204
|
+
return Math.round(tagCoverage * 100);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get skill recommendations based on detected tech stack.
|
|
209
|
+
* @param {string} projectRoot - Project root directory
|
|
210
|
+
* @param {Object} [options] - Options
|
|
211
|
+
* @param {string[]} [options.installedSkills] - Names of already-installed skills to filter out
|
|
212
|
+
* @param {number} [options.minScore] - Minimum relevance score (default: 20)
|
|
213
|
+
* @param {number} [options.maxResults] - Maximum results per category (default: 5)
|
|
214
|
+
* @returns {Object} Recommendations with stack info and ranked skills
|
|
215
|
+
*/
|
|
216
|
+
function getRecommendations(projectRoot, options = {}) {
|
|
217
|
+
const { installedSkills = [], minScore = 20, maxResults = 5 } = options;
|
|
218
|
+
|
|
219
|
+
const stack = detectTechStack(projectRoot);
|
|
220
|
+
const allSkills = getAllCuratedSkills();
|
|
221
|
+
const installedSet = new Set(installedSkills.map(n => n.toLowerCase()));
|
|
222
|
+
|
|
223
|
+
// Score all skills
|
|
224
|
+
const scored = allSkills
|
|
225
|
+
.map(skill => ({
|
|
226
|
+
...skill,
|
|
227
|
+
score: scoreSkill(skill, stack),
|
|
228
|
+
installed: installedSet.has(skill.name.toLowerCase()),
|
|
229
|
+
}))
|
|
230
|
+
.filter(s => s.score >= minScore && !s.installed)
|
|
231
|
+
.sort((a, b) => b.score - a.score);
|
|
232
|
+
|
|
233
|
+
// Group by category with limits
|
|
234
|
+
const byCategory = {};
|
|
235
|
+
for (const skill of scored) {
|
|
236
|
+
if (!byCategory[skill.category]) {
|
|
237
|
+
byCategory[skill.category] = [];
|
|
238
|
+
}
|
|
239
|
+
if (byCategory[skill.category].length < maxResults) {
|
|
240
|
+
byCategory[skill.category].push(skill);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
stack,
|
|
246
|
+
recommendations: byCategory,
|
|
247
|
+
totalMatches: scored.length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Format recommendations as a display string.
|
|
253
|
+
* @param {Object} result - Result from getRecommendations
|
|
254
|
+
* @returns {string} Formatted display text
|
|
255
|
+
*/
|
|
256
|
+
function formatRecommendations(result) {
|
|
257
|
+
const { stack, recommendations, totalMatches } = result;
|
|
258
|
+
|
|
259
|
+
const lines = [];
|
|
260
|
+
|
|
261
|
+
// Show detected stack
|
|
262
|
+
const detected = [];
|
|
263
|
+
if (stack.frameworks.length) detected.push(`Frameworks: ${stack.frameworks.join(', ')}`);
|
|
264
|
+
if (stack.languages.length) detected.push(`Languages: ${stack.languages.join(', ')}`);
|
|
265
|
+
if (stack.databases.length) detected.push(`Databases: ${stack.databases.join(', ')}`);
|
|
266
|
+
if (stack.testing.length) detected.push(`Testing: ${stack.testing.join(', ')}`);
|
|
267
|
+
if (stack.styling.length) detected.push(`Styling: ${stack.styling.join(', ')}`);
|
|
268
|
+
if (stack.devops.length) detected.push(`DevOps: ${stack.devops.join(', ')}`);
|
|
269
|
+
|
|
270
|
+
if (detected.length > 0) {
|
|
271
|
+
lines.push('**Detected Tech Stack:**');
|
|
272
|
+
for (const d of detected) {
|
|
273
|
+
lines.push(`- ${d}`);
|
|
274
|
+
}
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Show recommendations by category
|
|
279
|
+
const categories = Object.keys(recommendations);
|
|
280
|
+
if (categories.length === 0) {
|
|
281
|
+
lines.push('No matching skills found for your tech stack.');
|
|
282
|
+
lines.push('Browse the full marketplace: `npx skills find`');
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
lines.push(`**Recommended Skills (${totalMatches} matches):**`);
|
|
287
|
+
lines.push('');
|
|
288
|
+
|
|
289
|
+
for (const category of categories) {
|
|
290
|
+
const skills = recommendations[category];
|
|
291
|
+
lines.push(`### ${category}`);
|
|
292
|
+
lines.push('');
|
|
293
|
+
for (const s of skills) {
|
|
294
|
+
lines.push(`- **${s.name}** (${s.score}% match) - ${s.description}`);
|
|
295
|
+
lines.push(` Install: \`${s.installCmd}\``);
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
lines.push('---');
|
|
301
|
+
lines.push('Browse more: `npx skills find`');
|
|
302
|
+
|
|
303
|
+
return lines.join('\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
detectTechStack,
|
|
308
|
+
scoreSkill,
|
|
309
|
+
getRecommendations,
|
|
310
|
+
formatRecommendations,
|
|
311
|
+
};
|