@birdapi/velinstyle 1.2.2 → 1.3.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 (109) hide show
  1. package/README.de.md +18 -4
  2. package/README.md +42 -7
  3. package/cli/cli-manifest.json +441 -174
  4. package/cli/docgen/extract-cli.js +2 -1
  5. package/cli/docs-generate.js +9 -0
  6. package/cli/experience.js +173 -0
  7. package/cli/index.js +1294 -417
  8. package/cli/motion.js +738 -0
  9. package/cli/production/extract.js +46 -6
  10. package/cli/production/report.js +104 -14
  11. package/cli/production/run.js +44 -1
  12. package/cli/security.js +190 -0
  13. package/cli/skills.js +53 -10
  14. package/cli/transparency.js +140 -41
  15. package/cli/workflow.js +32 -17
  16. package/components/velin-theme-toggle.js +15 -1
  17. package/core/a11y/component-contracts.json +391 -274
  18. package/core/attributes/registry.js +1 -1
  19. package/core/motion/analyze.js +38 -0
  20. package/core/motion/blueprint.js +51 -0
  21. package/core/motion/composer.js +114 -0
  22. package/core/motion/data/registry.json +352 -0
  23. package/core/motion/effects.js +17 -1
  24. package/core/motion/index.js +112 -9
  25. package/core/motion/optimize.js +44 -0
  26. package/core/motion/policy.js +77 -0
  27. package/core/motion/registry.js +131 -0
  28. package/core/motion/review.js +59 -0
  29. package/core/motion/scan.js +150 -0
  30. package/core/motion/timeline.js +68 -0
  31. package/core/motion/transitions.js +49 -0
  32. package/core/motion/triggers.js +168 -0
  33. package/core/security/checks/fs.js +92 -0
  34. package/core/security/detect.js +35 -0
  35. package/core/security/engine.js +116 -0
  36. package/core/security/index.js +18 -0
  37. package/core/security/registry.js +85 -0
  38. package/core/security/report/report.js +113 -0
  39. package/core/security/rules/ci/index.js +146 -0
  40. package/core/security/rules/consumer/index.js +174 -0
  41. package/core/security/rules/deps/index.js +182 -0
  42. package/core/security/rules/index.js +24 -0
  43. package/core/security/rules/publish/index.js +230 -0
  44. package/core/security/rules/repo/index.js +143 -0
  45. package/core/security/rules/secrets/index.js +133 -0
  46. package/core/security/score/dependency-health.js +53 -0
  47. package/core/security/score/release-health.js +66 -0
  48. package/core/security/score/score.js +102 -0
  49. package/core/security/timeline/timeline.js +76 -0
  50. package/dist/chunks/attributes-HK7VIOLA.js +1080 -0
  51. package/dist/chunks/attributes-VRHHVNDO.js +1080 -0
  52. package/dist/chunks/chunk-MYKUI364.js +116 -0
  53. package/dist/chunks/chunk-NEVBPD5T.js +116 -0
  54. package/dist/chunks/runtime-entry.js +1 -1
  55. package/dist/chunks/velin-theme-toggle-J2NHC7IM.js +304 -0
  56. package/dist/llms.txt +2 -2
  57. package/dist/search-index.json +166 -2
  58. package/dist/velin-agent.json +156 -32
  59. package/dist/velinstyle-components.iife.js +862 -17
  60. package/dist/velinstyle-components.js +862 -17
  61. package/dist/velinstyle-components.min.js +126 -126
  62. package/dist/velinstyle.css +42 -7
  63. package/dist/velinstyle.min.css +1 -1
  64. package/package.json +158 -143
  65. package/packages/velinstyle-cli-core/package.json +11 -0
  66. package/packages/velinstyle-cli-core/src/config.js +84 -0
  67. package/packages/velinstyle-cli-core/src/contract.js +56 -0
  68. package/packages/velinstyle-cli-core/src/event-bus.js +50 -0
  69. package/packages/velinstyle-cli-core/src/index.js +18 -0
  70. package/packages/velinstyle-cli-core/src/lifecycle.js +33 -0
  71. package/packages/velinstyle-cli-core/src/runner.js +129 -0
  72. package/packages/velinstyle-cli-registry/data/commands.json +1216 -0
  73. package/packages/velinstyle-cli-registry/data/layouts.json +33 -0
  74. package/packages/velinstyle-cli-registry/data/widgets.json +35 -0
  75. package/packages/velinstyle-cli-registry/package.json +13 -0
  76. package/packages/velinstyle-cli-registry/src/index.js +88 -0
  77. package/packages/velinstyle-cli-renderer/package.json +8 -0
  78. package/packages/velinstyle-cli-renderer/src/index.js +286 -0
  79. package/packages/velinstyle-cli-theme/package.json +8 -0
  80. package/packages/velinstyle-cli-theme/src/index.js +84 -0
  81. package/packages/velinstyle-cli-ui/package.json +8 -0
  82. package/packages/velinstyle-cli-ui/src/index.js +34 -0
  83. package/packages/velinstyle-motion/README.md +81 -0
  84. package/packages/velinstyle-motion/package.json +37 -0
  85. package/packages/velinstyle-motion/src/ai/mini-prompt.js +119 -0
  86. package/packages/velinstyle-motion/src/ai/provider.js +59 -0
  87. package/packages/velinstyle-motion/src/ai/providers/pixverse-setup.js +402 -0
  88. package/packages/velinstyle-motion/src/ai/providers/pixverse.js +245 -0
  89. package/packages/velinstyle-motion/src/config.js +257 -0
  90. package/packages/velinstyle-motion/src/export/html-clip.js +141 -0
  91. package/packages/velinstyle-motion/src/index.js +304 -0
  92. package/packages/velinstyle-motion/src/jobs/cache.js +30 -0
  93. package/packages/velinstyle-motion/src/jobs/queue.js +69 -0
  94. package/packages/velinstyle-motion/src/jobs/usage.js +53 -0
  95. package/packages/velinstyle-motion/src/local/effects.js +139 -0
  96. package/packages/velinstyle-motion/src/local/engine.js +96 -0
  97. package/packages/velinstyle-motion/src/providers-catalog.js +44 -0
  98. package/packages/velinstyle-motion/src/registry/presets.js +25 -0
  99. package/packages/velinstyle-motion/src/registry/presets.json +85 -0
  100. package/packages/velinstyle-motion/src/ux/cost-gate.js +37 -0
  101. package/packages/velinstyle-motion/src/ux/pixverse-gate.js +153 -0
  102. package/packages/velinstyle-motion/src/ux/wizard.js +171 -0
  103. package/packages/velinstyle-motion/src/validate-image.js +91 -0
  104. package/packages/velinstyle-skills/catalog.json +1 -1
  105. package/packages/velinstyle-skills/registry.json +1 -142
  106. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +1 -1
  107. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +1 -1
  108. package/src/tokens/motion.css +7 -0
  109. package/src/utilities/scroll-animation.css +66 -0
@@ -0,0 +1,230 @@
1
+ import { registerSecurityRule } from '../../registry.js';
2
+ import { fileExists, readJson, readText } from '../../checks/fs.js';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ export function registerPublishRules() {
7
+ registerSecurityRule({
8
+ id: 'publish.readme',
9
+ category: 'publish',
10
+ severity: 'error',
11
+ modes: ['repo', 'publish', 'audit'],
12
+ title: 'README',
13
+ run(ctx) {
14
+ if (fileExists(join(ctx.root, 'README.md'))) return [];
15
+ return [{
16
+ ruleId: 'publish.readme',
17
+ category: 'publish',
18
+ severity: 'error',
19
+ message: 'README.md missing',
20
+ path: 'README.md',
21
+ }];
22
+ },
23
+ });
24
+
25
+ registerSecurityRule({
26
+ id: 'publish.changelog',
27
+ category: 'publish',
28
+ severity: 'error',
29
+ modes: ['repo', 'publish', 'audit'],
30
+ title: 'CHANGELOG',
31
+ run(ctx) {
32
+ if (fileExists(join(ctx.root, 'CHANGELOG.md'))) return [];
33
+ return [{
34
+ ruleId: 'publish.changelog',
35
+ category: 'publish',
36
+ severity: 'error',
37
+ message: 'CHANGELOG.md missing',
38
+ path: 'CHANGELOG.md',
39
+ }];
40
+ },
41
+ });
42
+
43
+ registerSecurityRule({
44
+ id: 'publish.security-md',
45
+ category: 'publish',
46
+ severity: 'error',
47
+ modes: ['repo', 'publish', 'audit'],
48
+ title: 'SECURITY.md',
49
+ run(ctx) {
50
+ if (fileExists(join(ctx.root, 'SECURITY.md'))) return [];
51
+ return [{
52
+ ruleId: 'publish.security-md',
53
+ category: 'publish',
54
+ severity: 'error',
55
+ message: 'SECURITY.md missing',
56
+ path: 'SECURITY.md',
57
+ }];
58
+ },
59
+ });
60
+
61
+ registerSecurityRule({
62
+ id: 'publish.license',
63
+ category: 'publish',
64
+ severity: 'error',
65
+ modes: ['repo', 'publish', 'audit'],
66
+ title: 'LICENSE',
67
+ run(ctx) {
68
+ const pkg = readJson(join(ctx.root, 'package.json')) || {};
69
+ if (fileExists(join(ctx.root, 'LICENSE')) || fileExists(join(ctx.root, 'LICENSE.md')) || pkg.license) {
70
+ return [];
71
+ }
72
+ return [{
73
+ ruleId: 'publish.license',
74
+ category: 'publish',
75
+ severity: 'error',
76
+ message: 'LICENSE file / package.json license missing',
77
+ path: 'LICENSE',
78
+ }];
79
+ },
80
+ });
81
+
82
+ registerSecurityRule({
83
+ id: 'publish.release-notes',
84
+ category: 'publish',
85
+ severity: 'warning',
86
+ modes: ['repo', 'publish', 'audit'],
87
+ title: 'Release notes',
88
+ run(ctx) {
89
+ const pkg = readJson(join(ctx.root, 'package.json')) || {};
90
+ const v = pkg.version;
91
+ if (!v) return [];
92
+ const notes = fileExists(join(ctx.root, `RELEASE_NOTES_${v}.md`));
93
+ const cl = readText(join(ctx.root, 'CHANGELOG.md')) || '';
94
+ if (notes || cl.includes(`[${v}]`)) return [];
95
+ return [{
96
+ ruleId: 'publish.release-notes',
97
+ category: 'publish',
98
+ severity: 'warning',
99
+ message: `No RELEASE_NOTES_${v}.md and no CHANGELOG entry for [${v}]`,
100
+ fix: 'Add release notes before publish',
101
+ }];
102
+ },
103
+ });
104
+
105
+ registerSecurityRule({
106
+ id: 'publish.version-tag',
107
+ category: 'publish',
108
+ severity: 'warning',
109
+ modes: ['repo', 'publish', 'audit'],
110
+ title: 'Git tag matches version',
111
+ run(ctx) {
112
+ const pkg = readJson(join(ctx.root, 'package.json')) || {};
113
+ const v = pkg.version;
114
+ if (!v) return [];
115
+ try {
116
+ const tag = execSync(`git tag -l "v${v}"`, { cwd: ctx.root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
117
+ if (tag) return [];
118
+ return [{
119
+ ruleId: 'publish.version-tag',
120
+ category: 'publish',
121
+ severity: 'warning',
122
+ message: `Git tag v${v} not found yet`,
123
+ fix: `Create annotated tag v${v} before npm publish from CI`,
124
+ }];
125
+ } catch {
126
+ return [];
127
+ }
128
+ },
129
+ });
130
+
131
+ registerSecurityRule({
132
+ id: 'publish.clean-tree',
133
+ category: 'publish',
134
+ severity: 'warning',
135
+ modes: ['repo', 'publish', 'audit'],
136
+ title: 'Working tree clean',
137
+ run(ctx) {
138
+ try {
139
+ const dirty = execSync('git status --porcelain', { cwd: ctx.root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
140
+ if (!dirty) return [];
141
+ return [{
142
+ ruleId: 'publish.clean-tree',
143
+ category: 'publish',
144
+ severity: 'warning',
145
+ message: 'Working tree has uncommitted changes',
146
+ fix: 'Commit or stash before release',
147
+ }];
148
+ } catch {
149
+ return [];
150
+ }
151
+ },
152
+ });
153
+
154
+ registerSecurityRule({
155
+ id: 'publish.private-flag',
156
+ category: 'publish',
157
+ severity: 'error',
158
+ modes: ['repo', 'publish', 'audit'],
159
+ title: 'Not private',
160
+ run(ctx) {
161
+ const pkg = readJson(join(ctx.root, 'package.json')) || {};
162
+ if (pkg.private === true) {
163
+ return [{
164
+ ruleId: 'publish.private-flag',
165
+ category: 'publish',
166
+ severity: 'error',
167
+ message: 'package.json has "private": true — npm publish will fail',
168
+ path: 'package.json',
169
+ }];
170
+ }
171
+ return [];
172
+ },
173
+ });
174
+
175
+ registerSecurityRule({
176
+ id: 'publish.pack-dry-run',
177
+ category: 'publish',
178
+ severity: 'error',
179
+ modes: ['repo', 'publish', 'audit'],
180
+ title: 'npm pack dry-run',
181
+ run(ctx) {
182
+ if (!fileExists(join(ctx.root, 'package.json'))) {
183
+ return [{
184
+ ruleId: 'publish.pack-dry-run',
185
+ category: 'publish',
186
+ severity: 'warning',
187
+ message: 'package.json missing — cannot run npm pack --dry-run',
188
+ path: 'package.json',
189
+ }];
190
+ }
191
+ try {
192
+ execSync('npm pack --dry-run', {
193
+ cwd: ctx.root,
194
+ encoding: 'utf-8',
195
+ timeout: 30_000,
196
+ stdio: ['ignore', 'pipe', 'pipe'],
197
+ });
198
+ return [];
199
+ } catch (err) {
200
+ const status = err?.status;
201
+ const stderr = String(err?.stderr || err?.message || '').trim().slice(0, 400);
202
+ if (err?.code === 'ENOENT') {
203
+ return [{
204
+ ruleId: 'publish.pack-dry-run',
205
+ category: 'publish',
206
+ severity: 'warning',
207
+ message: 'npm not available — skipped pack dry-run',
208
+ fix: 'Install npm or run pack dry-run on a machine with Node/npm',
209
+ }];
210
+ }
211
+ if (err?.killed || err?.signal === 'SIGTERM' || /ETIMEDOUT|timed out/i.test(String(err?.message))) {
212
+ return [{
213
+ ruleId: 'publish.pack-dry-run',
214
+ category: 'publish',
215
+ severity: 'warning',
216
+ message: 'npm pack --dry-run timed out',
217
+ fix: 'Retry with network available or inspect package files locally',
218
+ }];
219
+ }
220
+ return [{
221
+ ruleId: 'publish.pack-dry-run',
222
+ category: 'publish',
223
+ severity: status ? 'error' : 'warning',
224
+ message: `npm pack --dry-run failed${status != null ? ` (exit ${status})` : ''}${stderr ? `: ${stderr}` : ''}`,
225
+ fix: 'Fix pack errors (files, package.json) before publish',
226
+ }];
227
+ }
228
+ },
229
+ });
230
+ }
@@ -0,0 +1,143 @@
1
+ import { registerSecurityRule } from '../../registry.js';
2
+ import { fileExists, readJson, readText, walkFiles, rel } from '../../checks/fs.js';
3
+ import { join } from 'path';
4
+
5
+ export function registerRepoRules() {
6
+ registerSecurityRule({
7
+ id: 'repo.package-json',
8
+ category: 'repo',
9
+ severity: 'error',
10
+ modes: ['repo', 'consumer', 'audit'],
11
+ title: 'package.json',
12
+ run(ctx) {
13
+ if (fileExists(join(ctx.root, 'package.json'))) return [];
14
+ return [{
15
+ ruleId: 'repo.package-json',
16
+ category: 'repo',
17
+ severity: 'error',
18
+ message: 'package.json missing',
19
+ path: 'package.json',
20
+ }];
21
+ },
22
+ });
23
+
24
+ registerSecurityRule({
25
+ id: 'repo.codeowners',
26
+ category: 'repo',
27
+ severity: 'info',
28
+ modes: ['repo', 'audit'],
29
+ title: 'CODEOWNERS',
30
+ run(ctx) {
31
+ if (
32
+ fileExists(join(ctx.root, 'CODEOWNERS'))
33
+ || fileExists(join(ctx.root, '.github', 'CODEOWNERS'))
34
+ ) return [];
35
+ return [{
36
+ ruleId: 'repo.codeowners',
37
+ category: 'repo',
38
+ severity: 'info',
39
+ message: 'CODEOWNERS not found',
40
+ fix: 'Add .github/CODEOWNERS for review ownership',
41
+ }];
42
+ },
43
+ });
44
+
45
+ registerSecurityRule({
46
+ id: 'repo.dependabot',
47
+ category: 'repo',
48
+ severity: 'info',
49
+ modes: ['repo', 'audit'],
50
+ title: 'Dependabot / Renovate',
51
+ run(ctx) {
52
+ if (
53
+ fileExists(join(ctx.root, '.github', 'dependabot.yml'))
54
+ || fileExists(join(ctx.root, 'renovate.json'))
55
+ || fileExists(join(ctx.root, '.github', 'renovate.json'))
56
+ ) return [];
57
+ return [{
58
+ ruleId: 'repo.dependabot',
59
+ category: 'repo',
60
+ severity: 'info',
61
+ message: 'No Dependabot/Renovate config',
62
+ fix: 'Enable dependency update automation',
63
+ }];
64
+ },
65
+ });
66
+
67
+ registerSecurityRule({
68
+ id: 'repo.backup-files',
69
+ category: 'repo',
70
+ severity: 'warning',
71
+ modes: ['repo', 'consumer', 'audit', 'scan'],
72
+ title: 'Backup / debug files',
73
+ run(ctx) {
74
+ const files = walkFiles(
75
+ ctx.root,
76
+ (n) => /\.(bak|old|tmp|swp)$/i.test(n) || /^debug\.log$/i.test(n),
77
+ 40,
78
+ );
79
+ return files.map((abs) => ({
80
+ ruleId: 'repo.backup-files',
81
+ category: 'repo',
82
+ severity: 'warning',
83
+ message: 'Backup/debug file in tree',
84
+ path: rel(ctx.root, abs),
85
+ fix: 'Delete or gitignore',
86
+ }));
87
+ },
88
+ });
89
+
90
+ registerSecurityRule({
91
+ id: 'repo.exec-spawn',
92
+ category: 'repo',
93
+ severity: 'info',
94
+ modes: ['repo', 'audit'],
95
+ title: 'child_process usage',
96
+ run(ctx) {
97
+ const files = walkFiles(
98
+ ctx.root,
99
+ (n, abs) => /\.(js|mjs|cjs)$/i.test(n) && !abs.includes(`${join('node_modules')}`),
100
+ 120,
101
+ );
102
+ const findings = [];
103
+ for (const abs of files) {
104
+ const t = readText(abs);
105
+ if (!t) continue;
106
+ if (/child_process|execSync|spawnSync|\bexec\(|\bspawn\(/.test(t)) {
107
+ const path = rel(ctx.root, abs);
108
+ if (path.startsWith('cli/') || path.startsWith('scripts/') || path.startsWith('core/security/')) {
109
+ continue; // allowlisted tooling
110
+ }
111
+ findings.push({
112
+ ruleId: 'repo.exec-spawn',
113
+ category: 'repo',
114
+ severity: 'info',
115
+ message: 'Process spawn/exec detected — review for injection',
116
+ path,
117
+ });
118
+ }
119
+ }
120
+ return findings.slice(0, 25);
121
+ },
122
+ });
123
+
124
+ registerSecurityRule({
125
+ id: 'repo.publishconfig',
126
+ category: 'repo',
127
+ severity: 'info',
128
+ modes: ['repo', 'publish', 'audit'],
129
+ title: 'publishConfig',
130
+ run(ctx) {
131
+ const pkg = readJson(join(ctx.root, 'package.json'));
132
+ if (!pkg || pkg.name !== '@birdapi/velinstyle') return [];
133
+ if (pkg.publishConfig?.access === 'public') return [];
134
+ return [{
135
+ ruleId: 'repo.publishconfig',
136
+ category: 'repo',
137
+ severity: 'info',
138
+ message: 'Consider publishConfig.access=public for scoped package',
139
+ path: 'package.json',
140
+ }];
141
+ },
142
+ });
143
+ }
@@ -0,0 +1,133 @@
1
+ import { registerSecurityRule } from '../../registry.js';
2
+ import { fileExists, readText, walkFiles, rel } from '../../checks/fs.js';
3
+ import { join } from 'path';
4
+
5
+ const SECRET_RE =
6
+ /(?:api[_-]?key|secret|token|password|passwd|auth[_-]?token)\s*[=:]\s*['"][^'"]{8,}['"]/i;
7
+ const AWS_RE = /AKIA[0-9A-Z]{16}/;
8
+ const PRIVATE_KEY = /-----BEGIN (?:RSA |OPENSSH )?PRIVATE KEY-----/;
9
+
10
+ export function registerSecretsRules() {
11
+ registerSecurityRule({
12
+ id: 'secrets.env-tracked',
13
+ category: 'secrets',
14
+ severity: 'error',
15
+ modes: ['repo', 'consumer', 'secrets', 'audit', 'publish'],
16
+ title: 'Tracked .env',
17
+ description: '.env must not be committed.',
18
+ run(ctx) {
19
+ const findings = [];
20
+ for (const name of ['.env', '.env.local', '.env.production']) {
21
+ const p = join(ctx.root, name);
22
+ if (fileExists(p)) {
23
+ findings.push({
24
+ ruleId: 'secrets.env-tracked',
25
+ category: 'secrets',
26
+ severity: 'error',
27
+ message: `${name} exists in project root — ensure it is gitignored and not committed`,
28
+ path: name,
29
+ fix: 'Add to .gitignore; use secrets manager / CI secrets',
30
+ });
31
+ }
32
+ }
33
+ return findings;
34
+ },
35
+ });
36
+
37
+ registerSecurityRule({
38
+ id: 'secrets.gitignore-env',
39
+ category: 'secrets',
40
+ severity: 'warning',
41
+ modes: ['repo', 'consumer', 'secrets', 'audit'],
42
+ title: '.gitignore covers .env',
43
+ run(ctx) {
44
+ const gi = readText(join(ctx.root, '.gitignore')) || '';
45
+ if (!gi.includes('.env')) {
46
+ return [{
47
+ ruleId: 'secrets.gitignore-env',
48
+ category: 'secrets',
49
+ severity: 'warning',
50
+ message: '.gitignore does not mention .env',
51
+ path: '.gitignore',
52
+ fix: 'Add .env and .env.* to .gitignore',
53
+ }];
54
+ }
55
+ return [];
56
+ },
57
+ });
58
+
59
+ registerSecurityRule({
60
+ id: 'secrets.npmrc-auth',
61
+ category: 'secrets',
62
+ severity: 'error',
63
+ modes: ['repo', 'consumer', 'secrets', 'audit', 'publish'],
64
+ title: 'npmrc auth token',
65
+ run(ctx) {
66
+ const findings = [];
67
+ for (const name of ['.npmrc', '.npmrc.local']) {
68
+ const t = readText(join(ctx.root, name));
69
+ if (t && /_authToken|_auth\s*=|\/\/.*:_password=/i.test(t)) {
70
+ findings.push({
71
+ ruleId: 'secrets.npmrc-auth',
72
+ category: 'secrets',
73
+ severity: 'error',
74
+ message: `${name} contains auth credentials`,
75
+ path: name,
76
+ fix: 'Use env NPM_TOKEN; never commit tokens',
77
+ });
78
+ }
79
+ }
80
+ return findings;
81
+ },
82
+ });
83
+
84
+ registerSecurityRule({
85
+ id: 'secrets.hardcoded',
86
+ category: 'secrets',
87
+ severity: 'error',
88
+ modes: ['repo', 'consumer', 'secrets', 'scan', 'audit'],
89
+ title: 'Hardcoded secrets',
90
+ run(ctx) {
91
+ const findings = [];
92
+ const files = walkFiles(
93
+ ctx.root,
94
+ (n) => /\.(js|mjs|cjs|ts|json|env|yml|yaml|sh|ps1|html)$/i.test(n),
95
+ 200,
96
+ );
97
+ for (const abs of files) {
98
+ const t = readText(abs);
99
+ if (!t) continue;
100
+ if (PRIVATE_KEY.test(t) || AWS_RE.test(t) || SECRET_RE.test(t)) {
101
+ findings.push({
102
+ ruleId: 'secrets.hardcoded',
103
+ category: 'secrets',
104
+ severity: 'error',
105
+ message: 'Possible hardcoded secret or private key',
106
+ path: rel(ctx.root, abs),
107
+ fix: 'Remove secret; rotate if exposed',
108
+ });
109
+ }
110
+ }
111
+ return findings;
112
+ },
113
+ });
114
+
115
+ registerSecurityRule({
116
+ id: 'secrets.pem-files',
117
+ category: 'secrets',
118
+ severity: 'error',
119
+ modes: ['repo', 'consumer', 'secrets', 'audit'],
120
+ title: 'PEM / key files',
121
+ run(ctx) {
122
+ const files = walkFiles(ctx.root, (n) => /\.(pem|key|p12|pfx)$/i.test(n), 50);
123
+ return files.map((abs) => ({
124
+ ruleId: 'secrets.pem-files',
125
+ category: 'secrets',
126
+ severity: 'error',
127
+ message: 'Key/certificate file present in tree',
128
+ path: rel(ctx.root, abs),
129
+ fix: 'Store outside repo / use secret store',
130
+ }));
131
+ },
132
+ });
133
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Dependency Health summary from findings + package.json heuristics.
3
+ * Note: "outdated" here is heuristic (message/code), not live `npm outdated`.
4
+ */
5
+ import { join } from 'path';
6
+ import { fileExists, readJson } from '../checks/fs.js';
7
+
8
+ /**
9
+ * @param {string} root
10
+ * @param {import('../registry.js').SecurityFinding[]} findings
11
+ */
12
+ export function computeDependencyHealth(root, findings) {
13
+ const depFindings = findings.filter(
14
+ (f) =>
15
+ (f.category === 'deps' || f.ruleId?.startsWith('deps.'))
16
+ || f.category === 'malware'
17
+ || f.ruleId?.includes('lifecycle')
18
+ || f.ruleId?.includes('lockfile')
19
+ || f.ruleId?.includes('typosquat'),
20
+ );
21
+
22
+ const pkg = readJson(join(root, 'package.json')) || {};
23
+ const hasLock = fileExists(join(root, 'package-lock.json')) || fileExists(join(root, 'pnpm-lock.yaml')) || fileExists(join(root, 'yarn.lock'));
24
+
25
+ let deprecated = depFindings.filter((f) => /deprecat/i.test(f.message) || f.code === 'deprecated').length;
26
+ let noLockfile = depFindings.filter((f) => f.code === 'no-lockfile' || f.ruleId === 'deps.lockfile').length;
27
+ if (!hasLock && noLockfile === 0) noLockfile = 1;
28
+ // Heuristic "outdated" only when explicitly coded/messaged — not lockfile absence
29
+ let outdated = depFindings.filter((f) =>
30
+ f.code === 'outdated' || (/outdated/i.test(f.message) && f.code !== 'no-lockfile'),
31
+ ).length;
32
+ let critical = depFindings.filter((f) => f.severity === 'error').length;
33
+
34
+ let score = 100;
35
+ score -= critical * 20;
36
+ score -= deprecated * 8;
37
+ score -= outdated * 5;
38
+ score -= noLockfile * 15;
39
+ if (!pkg.engines?.node) score -= 3;
40
+ score = Math.max(0, Math.min(100, score));
41
+
42
+ return {
43
+ score,
44
+ deprecated,
45
+ outdated,
46
+ noLockfile,
47
+ critical,
48
+ hasLockfile: hasLock,
49
+ hasEngines: Boolean(pkg.engines?.node),
50
+ note: 'outdated is heuristic only (no live npm outdated)',
51
+ gate: critical > 0 ? 'fail' : score < 80 ? 'warn' : 'pass',
52
+ };
53
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Release Health — pre-publish gate summary.
3
+ */
4
+ import { join } from 'path';
5
+ import { fileExists, readJson, readText } from '../checks/fs.js';
6
+ import { execSync } from 'child_process';
7
+
8
+ /**
9
+ * @param {string} root
10
+ * @param {import('../registry.js').SecurityFinding[]} findings
11
+ */
12
+ export function computeReleaseHealth(root, findings) {
13
+ const pkg = readJson(join(root, 'package.json')) || {};
14
+ const version = pkg.version || '';
15
+
16
+ const checks = {
17
+ readme: fileExists(join(root, 'README.md')),
18
+ changelog: fileExists(join(root, 'CHANGELOG.md')),
19
+ license: fileExists(join(root, 'LICENSE')) || fileExists(join(root, 'LICENSE.md')) || Boolean(pkg.license),
20
+ securityMd: fileExists(join(root, 'SECURITY.md')),
21
+ releaseNotes:
22
+ fileExists(join(root, `RELEASE_NOTES_${version}.md`))
23
+ || (version && (readText(join(root, 'CHANGELOG.md')) || '').includes(`[${version}]`)),
24
+ provenance:
25
+ Boolean(pkg.publishConfig?.provenance)
26
+ || /id-token\s*:\s*write/.test(readText(join(root, '.github', 'workflows', 'publish-npm.yml')) || ''),
27
+ };
28
+
29
+ let tagOk = null;
30
+ let cleanTree = null;
31
+ try {
32
+ const tag = execSync(`git tag -l "v${version}"`, { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
33
+ tagOk = Boolean(tag);
34
+ } catch {
35
+ tagOk = null;
36
+ }
37
+ try {
38
+ const dirty = execSync('git status --porcelain', { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
39
+ cleanTree = dirty.length === 0;
40
+ } catch {
41
+ cleanTree = null;
42
+ }
43
+
44
+ const publishErrors = findings.filter(
45
+ (f) => (f.category === 'publish' || f.ruleId?.startsWith('publish.') || f.ruleId?.startsWith('release.')) && f.severity === 'error',
46
+ ).length;
47
+
48
+ const missing = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
49
+ let score = 100;
50
+ score -= missing.length * 10;
51
+ score -= publishErrors * 15;
52
+ if (tagOk === false) score -= 8;
53
+ if (cleanTree === false) score -= 5;
54
+ score = Math.max(0, Math.min(100, score));
55
+
56
+ const ready = publishErrors === 0 && checks.readme && checks.changelog && checks.license && checks.securityMd;
57
+ return {
58
+ score,
59
+ gate: ready ? (missing.length || cleanTree === false ? 'warn' : 'pass') : 'fail',
60
+ ready: ready && publishErrors === 0,
61
+ status: ready && publishErrors === 0 ? 'READY FOR PUBLISH' : 'BLOCKED',
62
+ checks: { ...checks, tagMatchesVersion: tagOk, workingTreeClean: cleanTree },
63
+ missing,
64
+ version,
65
+ };
66
+ }