stylemcp 0.1.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 (83) hide show
  1. package/README.md +261 -0
  2. package/action/action.yml +59 -0
  3. package/action/package.json +24 -0
  4. package/action/src/index.ts +455 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +335 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/rewriter/index.d.ts +45 -0
  14. package/dist/rewriter/index.d.ts.map +1 -0
  15. package/dist/rewriter/index.js +163 -0
  16. package/dist/rewriter/index.js.map +1 -0
  17. package/dist/schema/copy-patterns.d.ts +298 -0
  18. package/dist/schema/copy-patterns.d.ts.map +1 -0
  19. package/dist/schema/copy-patterns.js +53 -0
  20. package/dist/schema/copy-patterns.js.map +1 -0
  21. package/dist/schema/cta-rules.d.ts +274 -0
  22. package/dist/schema/cta-rules.d.ts.map +1 -0
  23. package/dist/schema/cta-rules.js +45 -0
  24. package/dist/schema/cta-rules.js.map +1 -0
  25. package/dist/schema/index.d.ts +2172 -0
  26. package/dist/schema/index.d.ts.map +1 -0
  27. package/dist/schema/index.js +92 -0
  28. package/dist/schema/index.js.map +1 -0
  29. package/dist/schema/tests.d.ts +274 -0
  30. package/dist/schema/tests.d.ts.map +1 -0
  31. package/dist/schema/tests.js +37 -0
  32. package/dist/schema/tests.js.map +1 -0
  33. package/dist/schema/tokens.d.ts +632 -0
  34. package/dist/schema/tokens.d.ts.map +1 -0
  35. package/dist/schema/tokens.js +68 -0
  36. package/dist/schema/tokens.js.map +1 -0
  37. package/dist/schema/voice.d.ts +280 -0
  38. package/dist/schema/voice.d.ts.map +1 -0
  39. package/dist/schema/voice.js +52 -0
  40. package/dist/schema/voice.js.map +1 -0
  41. package/dist/server/billing.d.ts +53 -0
  42. package/dist/server/billing.d.ts.map +1 -0
  43. package/dist/server/billing.js +216 -0
  44. package/dist/server/billing.js.map +1 -0
  45. package/dist/server/http.d.ts +5 -0
  46. package/dist/server/http.d.ts.map +1 -0
  47. package/dist/server/http.js +470 -0
  48. package/dist/server/http.js.map +1 -0
  49. package/dist/server/index.d.ts +3 -0
  50. package/dist/server/index.d.ts.map +1 -0
  51. package/dist/server/index.js +480 -0
  52. package/dist/server/index.js.map +1 -0
  53. package/dist/server/middleware/auth.d.ts +15 -0
  54. package/dist/server/middleware/auth.d.ts.map +1 -0
  55. package/dist/server/middleware/auth.js +83 -0
  56. package/dist/server/middleware/auth.js.map +1 -0
  57. package/dist/utils/pack-loader.d.ts +13 -0
  58. package/dist/utils/pack-loader.d.ts.map +1 -0
  59. package/dist/utils/pack-loader.js +98 -0
  60. package/dist/utils/pack-loader.js.map +1 -0
  61. package/dist/validator/index.d.ts +21 -0
  62. package/dist/validator/index.d.ts.map +1 -0
  63. package/dist/validator/index.js +60 -0
  64. package/dist/validator/index.js.map +1 -0
  65. package/dist/validator/rules/constraints.d.ts +8 -0
  66. package/dist/validator/rules/constraints.d.ts.map +1 -0
  67. package/dist/validator/rules/constraints.js +150 -0
  68. package/dist/validator/rules/constraints.js.map +1 -0
  69. package/dist/validator/rules/cta.d.ts +11 -0
  70. package/dist/validator/rules/cta.d.ts.map +1 -0
  71. package/dist/validator/rules/cta.js +113 -0
  72. package/dist/validator/rules/cta.js.map +1 -0
  73. package/dist/validator/rules/voice.d.ts +6 -0
  74. package/dist/validator/rules/voice.d.ts.map +1 -0
  75. package/dist/validator/rules/voice.js +106 -0
  76. package/dist/validator/rules/voice.js.map +1 -0
  77. package/package.json +61 -0
  78. package/packs/saas/copy_patterns.yaml +423 -0
  79. package/packs/saas/cta_rules.yaml +362 -0
  80. package/packs/saas/manifest.yaml +16 -0
  81. package/packs/saas/tests.yaml +305 -0
  82. package/packs/saas/tokens.json +226 -0
  83. package/packs/saas/voice.yaml +232 -0
@@ -0,0 +1,455 @@
1
+ import * as core from '@actions/core';
2
+ import * as github from '@actions/github';
3
+ import { glob } from 'glob';
4
+ import { readFile } from 'fs/promises';
5
+ import { join, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import * as yaml from 'js-yaml';
8
+
9
+ // Import from main package (will be bundled)
10
+ import { validate, ValidationResult, Violation } from '../../src/validator/index.js';
11
+ import { loadPack, getPacksDirectory } from '../../src/utils/pack-loader.js';
12
+ import { Pack } from '../../src/schema/index.js';
13
+
14
+ interface FileResult {
15
+ file: string;
16
+ result: ValidationResult;
17
+ }
18
+
19
+ async function run(): Promise<void> {
20
+ try {
21
+ // Get inputs
22
+ const packName = core.getInput('pack') || 'saas';
23
+ const filesPattern = core.getInput('files');
24
+ const directText = core.getInput('text');
25
+ const minScore = parseInt(core.getInput('min-score') || '70', 10);
26
+ const strict = core.getInput('strict') === 'true';
27
+ const failOnWarning = core.getInput('fail-on-warning') === 'true';
28
+ const contentType = core.getInput('content-type') as any;
29
+ const commentOnPr = core.getInput('comment-on-pr') === 'true';
30
+ const githubToken = core.getInput('github-token');
31
+
32
+ // Load pack
33
+ core.info(`Loading style pack: ${packName}`);
34
+ let pack: Pack;
35
+
36
+ try {
37
+ // Try loading as built-in pack first
38
+ const packPath = join(getPacksDirectory(), packName);
39
+ const result = await loadPack({ packPath });
40
+ pack = result.pack;
41
+
42
+ if (result.errors.length > 0) {
43
+ core.warning(`Pack loading warnings: ${result.errors.join(', ')}`);
44
+ }
45
+ } catch {
46
+ // Try loading as custom pack path
47
+ const result = await loadPack({ packPath: packName });
48
+ pack = result.pack;
49
+ }
50
+
51
+ core.info(`Loaded pack: ${pack.manifest.name} v${pack.manifest.version}`);
52
+
53
+ // Collect text to validate
54
+ const fileResults: FileResult[] = [];
55
+ let directResult: ValidationResult | null = null;
56
+
57
+ // Validate files
58
+ if (filesPattern) {
59
+ const files = await glob(filesPattern, { nodir: true });
60
+ core.info(`Found ${files.length} files matching pattern: ${filesPattern}`);
61
+
62
+ for (const file of files) {
63
+ const content = await readFile(file, 'utf-8');
64
+
65
+ // Extract text content based on file type
66
+ const textContent = extractTextContent(file, content);
67
+
68
+ if (textContent) {
69
+ const result = validate({
70
+ pack,
71
+ text: textContent,
72
+ context: { type: contentType },
73
+ });
74
+
75
+ fileResults.push({ file, result });
76
+ }
77
+ }
78
+ }
79
+
80
+ // Validate direct text
81
+ if (directText) {
82
+ directResult = validate({
83
+ pack,
84
+ text: directText,
85
+ context: { type: contentType },
86
+ });
87
+ }
88
+
89
+ // Aggregate results
90
+ const allResults = [
91
+ ...fileResults.map(fr => fr.result),
92
+ ...(directResult ? [directResult] : []),
93
+ ];
94
+
95
+ if (allResults.length === 0) {
96
+ core.warning('No text found to validate');
97
+ core.setOutput('score', 100);
98
+ core.setOutput('valid', true);
99
+ core.setOutput('violations', 0);
100
+ return;
101
+ }
102
+
103
+ // Calculate overall metrics
104
+ const avgScore = Math.round(
105
+ allResults.reduce((sum, r) => sum + r.score, 0) / allResults.length
106
+ );
107
+ const totalViolations = allResults.reduce((sum, r) => sum + r.violations.length, 0);
108
+ const totalErrors = allResults.reduce((sum, r) => sum + r.summary.errors, 0);
109
+ const totalWarnings = allResults.reduce((sum, r) => sum + r.summary.warnings, 0);
110
+
111
+ // Determine pass/fail
112
+ let valid = avgScore >= minScore && totalErrors === 0;
113
+ if (strict && totalViolations > 0) {
114
+ valid = false;
115
+ }
116
+ if (failOnWarning && totalWarnings > 0) {
117
+ valid = false;
118
+ }
119
+
120
+ // Set outputs
121
+ core.setOutput('score', avgScore);
122
+ core.setOutput('valid', valid);
123
+ core.setOutput('violations', totalViolations);
124
+ core.setOutput('report', JSON.stringify({
125
+ score: avgScore,
126
+ valid,
127
+ totalViolations,
128
+ totalErrors,
129
+ totalWarnings,
130
+ files: fileResults.map(fr => ({
131
+ file: fr.file,
132
+ score: fr.result.score,
133
+ violations: fr.result.violations.length,
134
+ })),
135
+ }));
136
+
137
+ // Log summary
138
+ core.info('');
139
+ core.info('=== StyleMCP Validation Results ===');
140
+ core.info(`Score: ${avgScore}/100`);
141
+ core.info(`Violations: ${totalViolations} (${totalErrors} errors, ${totalWarnings} warnings)`);
142
+ core.info(`Status: ${valid ? 'PASS' : 'FAIL'}`);
143
+ core.info('');
144
+
145
+ // Log file results
146
+ if (fileResults.length > 0) {
147
+ core.info('File Results:');
148
+ for (const { file, result } of fileResults) {
149
+ const status = result.valid ? '✓' : '✗';
150
+ core.info(` ${status} ${file}: ${result.score}/100 (${result.violations.length} violations)`);
151
+ }
152
+ core.info('');
153
+ }
154
+
155
+ // Log violations
156
+ if (totalViolations > 0) {
157
+ core.info('Violations:');
158
+ for (const { file, result } of fileResults) {
159
+ for (const violation of result.violations) {
160
+ const icon = violation.severity === 'error' ? '❌' : violation.severity === 'warning' ? '⚠️' : 'ℹ️';
161
+ core.info(` ${icon} [${file}] ${violation.message}`);
162
+ if (violation.text) {
163
+ core.info(` Text: "${violation.text}"`);
164
+ }
165
+ if (violation.suggestion) {
166
+ core.info(` Suggestion: ${violation.suggestion}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ if (directResult) {
172
+ for (const violation of directResult.violations) {
173
+ const icon = violation.severity === 'error' ? '❌' : violation.severity === 'warning' ? '⚠️' : 'ℹ️';
174
+ core.info(` ${icon} ${violation.message}`);
175
+ }
176
+ }
177
+ }
178
+
179
+ // Post PR comment if enabled
180
+ if (commentOnPr && github.context.payload.pull_request && githubToken) {
181
+ await postPrComment(githubToken, {
182
+ score: avgScore,
183
+ valid,
184
+ totalViolations,
185
+ totalErrors,
186
+ totalWarnings,
187
+ fileResults,
188
+ directResult,
189
+ packName: pack.manifest.name,
190
+ packVersion: pack.manifest.version,
191
+ });
192
+ }
193
+
194
+ // Fail the action if validation failed
195
+ if (!valid) {
196
+ core.setFailed(`StyleMCP validation failed. Score: ${avgScore}/100, Violations: ${totalViolations}`);
197
+ }
198
+ } catch (error) {
199
+ core.setFailed(error instanceof Error ? error.message : 'Unknown error');
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Extract text content from files based on type
205
+ */
206
+ function extractTextContent(file: string, content: string): string | null {
207
+ const ext = file.split('.').pop()?.toLowerCase();
208
+
209
+ switch (ext) {
210
+ case 'md':
211
+ case 'mdx':
212
+ // Return markdown content as-is
213
+ return content;
214
+
215
+ case 'json':
216
+ // Try to extract string values
217
+ try {
218
+ const obj = JSON.parse(content);
219
+ return extractStringsFromObject(obj).join('\n');
220
+ } catch {
221
+ return null;
222
+ }
223
+
224
+ case 'yaml':
225
+ case 'yml':
226
+ // Try to extract string values
227
+ try {
228
+ const obj = yaml.load(content);
229
+ return extractStringsFromObject(obj).join('\n');
230
+ } catch {
231
+ return null;
232
+ }
233
+
234
+ case 'tsx':
235
+ case 'jsx':
236
+ // Extract string literals from JSX
237
+ return extractJsxStrings(content);
238
+
239
+ case 'ts':
240
+ case 'js':
241
+ // Extract string literals
242
+ return extractJsStrings(content);
243
+
244
+ case 'txt':
245
+ return content;
246
+
247
+ default:
248
+ // Try to extract any quoted strings
249
+ return extractQuotedStrings(content);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Extract string values from an object recursively
255
+ */
256
+ function extractStringsFromObject(obj: any, strings: string[] = []): string[] {
257
+ if (typeof obj === 'string' && obj.length > 3) {
258
+ strings.push(obj);
259
+ } else if (Array.isArray(obj)) {
260
+ for (const item of obj) {
261
+ extractStringsFromObject(item, strings);
262
+ }
263
+ } else if (obj && typeof obj === 'object') {
264
+ for (const value of Object.values(obj)) {
265
+ extractStringsFromObject(value, strings);
266
+ }
267
+ }
268
+ return strings;
269
+ }
270
+
271
+ /**
272
+ * Extract string literals from JSX/TSX
273
+ */
274
+ function extractJsxStrings(content: string): string {
275
+ const strings: string[] = [];
276
+
277
+ // Match JSX text content between tags
278
+ const jsxTextRegex = />([^<>{]+)</g;
279
+ let match;
280
+ while ((match = jsxTextRegex.exec(content)) !== null) {
281
+ const text = match[1].trim();
282
+ if (text.length > 3 && !text.startsWith('{')) {
283
+ strings.push(text);
284
+ }
285
+ }
286
+
287
+ // Match string literals in props
288
+ const propRegex = /=["']([^"']+)["']/g;
289
+ while ((match = propRegex.exec(content)) !== null) {
290
+ const text = match[1].trim();
291
+ if (text.length > 3) {
292
+ strings.push(text);
293
+ }
294
+ }
295
+
296
+ return strings.join('\n');
297
+ }
298
+
299
+ /**
300
+ * Extract string literals from JS/TS
301
+ */
302
+ function extractJsStrings(content: string): string {
303
+ const strings: string[] = [];
304
+
305
+ // Match template literals
306
+ const templateRegex = /`([^`]+)`/g;
307
+ let match;
308
+ while ((match = templateRegex.exec(content)) !== null) {
309
+ const text = match[1].trim();
310
+ if (text.length > 10 && !text.includes('${')) {
311
+ strings.push(text);
312
+ }
313
+ }
314
+
315
+ // Match regular string literals (longer ones likely to be user-facing)
316
+ const stringRegex = /["']([^"'\n]{20,})["']/g;
317
+ while ((match = stringRegex.exec(content)) !== null) {
318
+ strings.push(match[1]);
319
+ }
320
+
321
+ return strings.join('\n');
322
+ }
323
+
324
+ /**
325
+ * Extract any quoted strings
326
+ */
327
+ function extractQuotedStrings(content: string): string {
328
+ const strings: string[] = [];
329
+ const regex = /["']([^"'\n]{10,})["']/g;
330
+ let match;
331
+ while ((match = regex.exec(content)) !== null) {
332
+ strings.push(match[1]);
333
+ }
334
+ return strings.join('\n');
335
+ }
336
+
337
+ /**
338
+ * Post a comment on the PR with validation results
339
+ */
340
+ async function postPrComment(
341
+ token: string,
342
+ data: {
343
+ score: number;
344
+ valid: boolean;
345
+ totalViolations: number;
346
+ totalErrors: number;
347
+ totalWarnings: number;
348
+ fileResults: FileResult[];
349
+ directResult: ValidationResult | null;
350
+ packName: string;
351
+ packVersion: string;
352
+ }
353
+ ): Promise<void> {
354
+ const octokit = github.getOctokit(token);
355
+ const { owner, repo } = github.context.repo;
356
+ const pullNumber = github.context.payload.pull_request!.number;
357
+
358
+ const statusIcon = data.valid ? '✅' : '❌';
359
+ const scoreColor = data.score >= 80 ? '🟢' : data.score >= 60 ? '🟡' : '🔴';
360
+
361
+ let body = `## ${statusIcon} StyleMCP Validation
362
+
363
+ ${scoreColor} **Score: ${data.score}/100**
364
+
365
+ | Metric | Count |
366
+ |--------|-------|
367
+ | Total Violations | ${data.totalViolations} |
368
+ | Errors | ${data.totalErrors} |
369
+ | Warnings | ${data.totalWarnings} |
370
+
371
+ `;
372
+
373
+ if (data.fileResults.length > 0) {
374
+ body += `### Files Checked
375
+
376
+ | File | Score | Violations |
377
+ |------|-------|------------|
378
+ `;
379
+ for (const { file, result } of data.fileResults) {
380
+ const icon = result.valid ? '✓' : '✗';
381
+ body += `| ${icon} ${file} | ${result.score} | ${result.violations.length} |\n`;
382
+ }
383
+ body += '\n';
384
+ }
385
+
386
+ if (data.totalViolations > 0) {
387
+ body += `### Violations
388
+
389
+ `;
390
+ const allViolations: { file?: string; violation: Violation }[] = [];
391
+
392
+ for (const { file, result } of data.fileResults) {
393
+ for (const violation of result.violations) {
394
+ allViolations.push({ file, violation });
395
+ }
396
+ }
397
+
398
+ if (data.directResult) {
399
+ for (const violation of data.directResult.violations) {
400
+ allViolations.push({ violation });
401
+ }
402
+ }
403
+
404
+ // Show top 10 violations
405
+ for (const { file, violation } of allViolations.slice(0, 10)) {
406
+ const icon = violation.severity === 'error' ? '🔴' : violation.severity === 'warning' ? '🟡' : '🔵';
407
+ body += `${icon} **${violation.message}**\n`;
408
+ if (file) {
409
+ body += ` 📁 ${file}\n`;
410
+ }
411
+ if (violation.text) {
412
+ body += ` > "${violation.text}"\n`;
413
+ }
414
+ if (violation.suggestion) {
415
+ body += ` 💡 ${violation.suggestion}\n`;
416
+ }
417
+ body += '\n';
418
+ }
419
+
420
+ if (allViolations.length > 10) {
421
+ body += `\n_...and ${allViolations.length - 10} more violations_\n`;
422
+ }
423
+ }
424
+
425
+ body += `\n---\n_Validated with [StyleMCP](https://stylemcp.com) pack: ${data.packName} v${data.packVersion}_`;
426
+
427
+ // Check for existing comment to update
428
+ const comments = await octokit.rest.issues.listComments({
429
+ owner,
430
+ repo,
431
+ issue_number: pullNumber,
432
+ });
433
+
434
+ const existingComment = comments.data.find(
435
+ comment => comment.body?.includes('## ') && comment.body?.includes('StyleMCP Validation')
436
+ );
437
+
438
+ if (existingComment) {
439
+ await octokit.rest.issues.updateComment({
440
+ owner,
441
+ repo,
442
+ comment_id: existingComment.id,
443
+ body,
444
+ });
445
+ } else {
446
+ await octokit.rest.issues.createComment({
447
+ owner,
448
+ repo,
449
+ issue_number: pullNumber,
450
+ body,
451
+ });
452
+ }
453
+ }
454
+
455
+ run();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}