javi-forge 1.6.0 → 1.7.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 (322) hide show
  1. package/README.md +45 -1
  2. package/ci-local/README.md +56 -11
  3. package/ci-local/ci-local.ps1 +445 -0
  4. package/ci-local/ci-local.sh +52 -9
  5. package/ci-local/docker/python.Dockerfile +7 -0
  6. package/ci-local/hooks/commit-msg +120 -44
  7. package/ci-local/hooks/commit-msg.test.sh +286 -0
  8. package/ci-local/hooks/pre-commit +9 -1
  9. package/ci-local/hooks/pre-push +13 -2
  10. package/ci-local/install.ps1 +367 -0
  11. package/ci-local/install.sh +151 -5
  12. package/ci-local/uninstall.ps1 +85 -0
  13. package/ci-local/uninstall.sh +95 -0
  14. package/dist/cli/dispatch/ci.d.ts +12 -0
  15. package/dist/cli/dispatch/ci.js +38 -0
  16. package/dist/cli/dispatch/security.d.ts +11 -0
  17. package/dist/cli/dispatch/security.js +68 -0
  18. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  19. package/dist/cli/dispatch/simple-renderers.js +65 -0
  20. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  21. package/dist/cli/dispatch/skill-publish.js +44 -0
  22. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  23. package/dist/cli/dispatch/skills-cmd.js +93 -0
  24. package/dist/cli/dispatch/tdd.d.ts +11 -0
  25. package/dist/cli/dispatch/tdd.js +45 -0
  26. package/dist/cli/dispatch/types.d.ts +29 -0
  27. package/dist/cli/dispatch/types.js +2 -0
  28. package/dist/cli/dispatch/workflow.d.ts +11 -0
  29. package/dist/cli/dispatch/workflow.js +67 -0
  30. package/dist/cli/help.d.ts +127 -0
  31. package/dist/cli/help.js +138 -0
  32. package/dist/cli/runtime.d.ts +16 -0
  33. package/dist/cli/runtime.js +38 -0
  34. package/dist/cli/validators.d.ts +17 -0
  35. package/dist/cli/validators.js +35 -0
  36. package/dist/commands/analyze.d.ts +1 -1
  37. package/dist/commands/analyze.js +14 -16
  38. package/dist/commands/ci.d.ts +50 -3
  39. package/dist/commands/ci.js +537 -182
  40. package/dist/commands/doctor.d.ts +2 -2
  41. package/dist/commands/doctor.js +112 -62
  42. package/dist/commands/init/report.d.ts +13 -0
  43. package/dist/commands/init/report.js +13 -0
  44. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  45. package/dist/commands/init/steps/agent-skills.js +45 -0
  46. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  47. package/dist/commands/init/steps/ai-sync.js +62 -0
  48. package/dist/commands/init/steps/ci.d.ts +24 -0
  49. package/dist/commands/init/steps/ci.js +74 -0
  50. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  51. package/dist/commands/init/steps/claude-md.js +54 -0
  52. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  53. package/dist/commands/init/steps/code-graph.js +68 -0
  54. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  55. package/dist/commands/init/steps/context-dir.js +47 -0
  56. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  57. package/dist/commands/init/steps/docker-deploy.js +60 -0
  58. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  59. package/dist/commands/init/steps/ghagga.js +58 -0
  60. package/dist/commands/init/steps/git.d.ts +25 -0
  61. package/dist/commands/init/steps/git.js +81 -0
  62. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  63. package/dist/commands/init/steps/gitignore.js +39 -0
  64. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  65. package/dist/commands/init/steps/local-ai.js +53 -0
  66. package/dist/commands/init/steps/manifest.d.ts +13 -0
  67. package/dist/commands/init/steps/manifest.js +58 -0
  68. package/dist/commands/init/steps/memory.d.ts +15 -0
  69. package/dist/commands/init/steps/memory.js +60 -0
  70. package/dist/commands/init/steps/mock.d.ts +13 -0
  71. package/dist/commands/init/steps/mock.js +64 -0
  72. package/dist/commands/init/steps/sdd.d.ts +12 -0
  73. package/dist/commands/init/steps/sdd.js +40 -0
  74. package/dist/commands/init/steps/security.d.ts +28 -0
  75. package/dist/commands/init/steps/security.js +103 -0
  76. package/dist/commands/init/types.d.ts +30 -0
  77. package/dist/commands/init/types.js +2 -0
  78. package/dist/commands/init.d.ts +5 -4
  79. package/dist/commands/init.js +42 -387
  80. package/dist/commands/llmstxt.d.ts +1 -1
  81. package/dist/commands/llmstxt.js +36 -34
  82. package/dist/commands/plugin.d.ts +10 -1
  83. package/dist/commands/plugin.js +92 -47
  84. package/dist/commands/security.d.ts +11 -5
  85. package/dist/commands/security.js +236 -77
  86. package/dist/commands/skills/analysis.d.ts +19 -0
  87. package/dist/commands/skills/analysis.js +211 -0
  88. package/dist/commands/skills/benchmark.d.ts +6 -0
  89. package/dist/commands/skills/benchmark.js +82 -0
  90. package/dist/commands/skills/constants.d.ts +10 -0
  91. package/dist/commands/skills/constants.js +27 -0
  92. package/dist/commands/skills/directives.d.ts +21 -0
  93. package/dist/commands/skills/directives.js +101 -0
  94. package/dist/commands/skills/parsing.d.ts +16 -0
  95. package/dist/commands/skills/parsing.js +108 -0
  96. package/dist/commands/skills/rules.d.ts +5 -0
  97. package/dist/commands/skills/rules.js +32 -0
  98. package/dist/commands/skills/scoring.d.ts +47 -0
  99. package/dist/commands/skills/scoring.js +306 -0
  100. package/dist/commands/skills.d.ts +8 -69
  101. package/dist/commands/skills.js +9 -490
  102. package/dist/commands/tdd-pipeline.d.ts +17 -0
  103. package/dist/commands/tdd-pipeline.js +144 -0
  104. package/dist/commands/tdd.d.ts +1 -1
  105. package/dist/commands/tdd.js +21 -18
  106. package/dist/commands/workflow.d.ts +23 -0
  107. package/dist/commands/workflow.js +116 -0
  108. package/dist/constants.d.ts +15 -1
  109. package/dist/constants.js +161 -122
  110. package/dist/index.js +43 -280
  111. package/dist/lib/agent-skills.d.ts +36 -1
  112. package/dist/lib/agent-skills.js +168 -19
  113. package/dist/lib/atlassian-mcp.d.ts +42 -0
  114. package/dist/lib/atlassian-mcp.js +98 -0
  115. package/dist/lib/auto-skill-install.d.ts +37 -0
  116. package/dist/lib/auto-skill-install.js +91 -0
  117. package/dist/lib/auto-wire.d.ts +20 -0
  118. package/dist/lib/auto-wire.js +240 -0
  119. package/dist/lib/ci-config.d.ts +65 -0
  120. package/dist/lib/ci-config.js +297 -0
  121. package/dist/lib/claudemd.d.ts +13 -1
  122. package/dist/lib/claudemd.js +174 -24
  123. package/dist/lib/codex-export.d.ts +1 -1
  124. package/dist/lib/codex-export.js +26 -32
  125. package/dist/lib/common.d.ts +1 -1
  126. package/dist/lib/common.js +52 -44
  127. package/dist/lib/context.d.ts +17 -2
  128. package/dist/lib/context.js +142 -13
  129. package/dist/lib/crash-recovery.d.ts +34 -0
  130. package/dist/lib/crash-recovery.js +123 -0
  131. package/dist/lib/docker.d.ts +23 -2
  132. package/dist/lib/docker.js +168 -131
  133. package/dist/lib/exec.d.ts +10 -0
  134. package/dist/lib/exec.js +11 -0
  135. package/dist/lib/frontmatter.d.ts +1 -1
  136. package/dist/lib/frontmatter.js +29 -15
  137. package/dist/lib/harness-audit.d.ts +35 -0
  138. package/dist/lib/harness-audit.js +277 -0
  139. package/dist/lib/parallel-batch.d.ts +42 -0
  140. package/dist/lib/parallel-batch.js +88 -0
  141. package/dist/lib/plugin.d.ts +9 -3
  142. package/dist/lib/plugin.js +127 -70
  143. package/dist/lib/secret-scanner.d.ts +30 -0
  144. package/dist/lib/secret-scanner.js +274 -0
  145. package/dist/lib/security-analysis.d.ts +74 -0
  146. package/dist/lib/security-analysis.js +489 -0
  147. package/dist/lib/skill-publish.d.ts +40 -0
  148. package/dist/lib/skill-publish.js +164 -0
  149. package/dist/lib/skill-scanner.d.ts +63 -0
  150. package/dist/lib/skill-scanner.js +383 -0
  151. package/dist/lib/stack-detector.d.ts +38 -0
  152. package/dist/lib/stack-detector.js +207 -0
  153. package/dist/lib/supply-chain.d.ts +23 -0
  154. package/dist/lib/supply-chain.js +126 -0
  155. package/dist/lib/team-presets.d.ts +53 -0
  156. package/dist/lib/team-presets.js +201 -0
  157. package/dist/lib/template.d.ts +16 -1
  158. package/dist/lib/template.js +46 -17
  159. package/dist/lib/workflow/discovery.d.ts +27 -0
  160. package/dist/lib/workflow/discovery.js +85 -0
  161. package/dist/lib/workflow/index.d.ts +5 -0
  162. package/dist/lib/workflow/index.js +5 -0
  163. package/dist/lib/workflow/parser.d.ts +16 -0
  164. package/dist/lib/workflow/parser.js +198 -0
  165. package/dist/lib/workflow/renderer.d.ts +9 -0
  166. package/dist/lib/workflow/renderer.js +152 -0
  167. package/dist/lib/workflow/validator.d.ts +10 -0
  168. package/dist/lib/workflow/validator.js +189 -0
  169. package/dist/tasks/index.d.ts +4 -0
  170. package/dist/tasks/index.js +4 -0
  171. package/dist/tasks/scaffold-tasks.d.ts +3 -0
  172. package/dist/tasks/scaffold-tasks.js +14 -0
  173. package/dist/tasks/task-id.d.ts +30 -0
  174. package/dist/tasks/task-id.js +59 -0
  175. package/dist/tasks/task-tracker.d.ts +15 -0
  176. package/dist/tasks/task-tracker.js +81 -0
  177. package/dist/types/index.d.ts +134 -6
  178. package/dist/types/index.js +11 -1
  179. package/dist/ui/AnalyzeUI.d.ts +1 -1
  180. package/dist/ui/AnalyzeUI.js +38 -39
  181. package/dist/ui/App.d.ts +5 -3
  182. package/dist/ui/App.js +86 -46
  183. package/dist/ui/AutoSkills.d.ts +9 -0
  184. package/dist/ui/AutoSkills.js +124 -0
  185. package/dist/ui/CI.d.ts +2 -2
  186. package/dist/ui/CI.js +24 -26
  187. package/dist/ui/CIContext.d.ts +1 -1
  188. package/dist/ui/CIContext.js +2 -2
  189. package/dist/ui/CISelector.d.ts +2 -2
  190. package/dist/ui/CISelector.js +23 -15
  191. package/dist/ui/Doctor.d.ts +1 -1
  192. package/dist/ui/Doctor.js +35 -29
  193. package/dist/ui/Header.d.ts +1 -1
  194. package/dist/ui/Header.js +14 -14
  195. package/dist/ui/HookProfileSelector.d.ts +9 -0
  196. package/dist/ui/HookProfileSelector.js +54 -0
  197. package/dist/ui/LlmsTxt.d.ts +1 -1
  198. package/dist/ui/LlmsTxt.js +31 -22
  199. package/dist/ui/MemorySelector.d.ts +2 -2
  200. package/dist/ui/MemorySelector.js +28 -16
  201. package/dist/ui/NameInput.d.ts +1 -1
  202. package/dist/ui/NameInput.js +21 -21
  203. package/dist/ui/OptionSelector.d.ts +6 -2
  204. package/dist/ui/OptionSelector.js +83 -32
  205. package/dist/ui/Plugin.d.ts +4 -3
  206. package/dist/ui/Plugin.js +78 -35
  207. package/dist/ui/Progress.d.ts +3 -3
  208. package/dist/ui/Progress.js +23 -22
  209. package/dist/ui/Skills.d.ts +2 -2
  210. package/dist/ui/Skills.js +61 -32
  211. package/dist/ui/StackSelector.d.ts +2 -2
  212. package/dist/ui/StackSelector.js +27 -17
  213. package/dist/ui/Summary.d.ts +3 -3
  214. package/dist/ui/Summary.js +60 -50
  215. package/dist/ui/Welcome.d.ts +1 -1
  216. package/dist/ui/Welcome.js +15 -16
  217. package/dist/ui/theme.d.ts +1 -1
  218. package/dist/ui/theme.js +6 -6
  219. package/lib/common.psm1 +167 -0
  220. package/lib/common.sh +30 -0
  221. package/package.json +46 -24
  222. package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
  223. package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
  224. package/templates/common/repoforge/repoforge.yaml +34 -0
  225. package/templates/github/deploy-docker-zero-downtime.yml +140 -0
  226. package/templates/github/repoforge-graph.yml +45 -0
  227. package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
  228. package/templates/local-ai/.env.example +17 -0
  229. package/templates/local-ai/docker-compose.yml +95 -0
  230. package/templates/security-hooks/claude-settings-security.json +30 -0
  231. package/templates/security-hooks/commit-msg-signing +29 -0
  232. package/templates/security-hooks/pre-commit-permissions +74 -0
  233. package/templates/security-hooks/pre-commit-secrets +74 -0
  234. package/templates/security-hooks/pre-push-branch-protection +62 -0
  235. package/templates/security-hooks/pre-push-deps +83 -0
  236. package/templates/security-hooks/pre-push-signing +67 -0
  237. package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
  238. package/templates/workflows/ci-pipeline.dot +15 -0
  239. package/templates/workflows/feature-flow.dot +21 -0
  240. package/templates/workflows/release.dot +16 -0
  241. package/dist/__integration__/helpers.d.ts +0 -20
  242. package/dist/__integration__/helpers.d.ts.map +0 -1
  243. package/dist/__integration__/helpers.js +0 -31
  244. package/dist/__integration__/helpers.js.map +0 -1
  245. package/dist/commands/analyze.d.ts.map +0 -1
  246. package/dist/commands/analyze.js.map +0 -1
  247. package/dist/commands/ci.d.ts.map +0 -1
  248. package/dist/commands/ci.js.map +0 -1
  249. package/dist/commands/doctor.d.ts.map +0 -1
  250. package/dist/commands/doctor.js.map +0 -1
  251. package/dist/commands/init.d.ts.map +0 -1
  252. package/dist/commands/init.js.map +0 -1
  253. package/dist/commands/llmstxt.d.ts.map +0 -1
  254. package/dist/commands/llmstxt.js.map +0 -1
  255. package/dist/commands/plugin.d.ts.map +0 -1
  256. package/dist/commands/plugin.js.map +0 -1
  257. package/dist/commands/security.d.ts.map +0 -1
  258. package/dist/commands/security.js.map +0 -1
  259. package/dist/commands/skills.d.ts.map +0 -1
  260. package/dist/commands/skills.js.map +0 -1
  261. package/dist/commands/tdd.d.ts.map +0 -1
  262. package/dist/commands/tdd.js.map +0 -1
  263. package/dist/constants.d.ts.map +0 -1
  264. package/dist/constants.js.map +0 -1
  265. package/dist/index.d.ts.map +0 -1
  266. package/dist/index.js.map +0 -1
  267. package/dist/lib/agent-skills.d.ts.map +0 -1
  268. package/dist/lib/agent-skills.js.map +0 -1
  269. package/dist/lib/claudemd.d.ts.map +0 -1
  270. package/dist/lib/claudemd.js.map +0 -1
  271. package/dist/lib/codex-export.d.ts.map +0 -1
  272. package/dist/lib/codex-export.js.map +0 -1
  273. package/dist/lib/common.d.ts.map +0 -1
  274. package/dist/lib/common.js.map +0 -1
  275. package/dist/lib/context.d.ts.map +0 -1
  276. package/dist/lib/context.js.map +0 -1
  277. package/dist/lib/docker.d.ts.map +0 -1
  278. package/dist/lib/docker.js.map +0 -1
  279. package/dist/lib/frontmatter.d.ts.map +0 -1
  280. package/dist/lib/frontmatter.js.map +0 -1
  281. package/dist/lib/plugin.d.ts.map +0 -1
  282. package/dist/lib/plugin.js.map +0 -1
  283. package/dist/lib/template.d.ts.map +0 -1
  284. package/dist/lib/template.js.map +0 -1
  285. package/dist/types/index.d.ts.map +0 -1
  286. package/dist/types/index.js.map +0 -1
  287. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  288. package/dist/ui/AnalyzeUI.js.map +0 -1
  289. package/dist/ui/App.d.ts.map +0 -1
  290. package/dist/ui/App.js.map +0 -1
  291. package/dist/ui/CI.d.ts.map +0 -1
  292. package/dist/ui/CI.js.map +0 -1
  293. package/dist/ui/CIContext.d.ts.map +0 -1
  294. package/dist/ui/CIContext.js.map +0 -1
  295. package/dist/ui/CISelector.d.ts.map +0 -1
  296. package/dist/ui/CISelector.js.map +0 -1
  297. package/dist/ui/Doctor.d.ts.map +0 -1
  298. package/dist/ui/Doctor.js.map +0 -1
  299. package/dist/ui/Header.d.ts.map +0 -1
  300. package/dist/ui/Header.js.map +0 -1
  301. package/dist/ui/LlmsTxt.d.ts.map +0 -1
  302. package/dist/ui/LlmsTxt.js.map +0 -1
  303. package/dist/ui/MemorySelector.d.ts.map +0 -1
  304. package/dist/ui/MemorySelector.js.map +0 -1
  305. package/dist/ui/NameInput.d.ts.map +0 -1
  306. package/dist/ui/NameInput.js.map +0 -1
  307. package/dist/ui/OptionSelector.d.ts.map +0 -1
  308. package/dist/ui/OptionSelector.js.map +0 -1
  309. package/dist/ui/Plugin.d.ts.map +0 -1
  310. package/dist/ui/Plugin.js.map +0 -1
  311. package/dist/ui/Progress.d.ts.map +0 -1
  312. package/dist/ui/Progress.js.map +0 -1
  313. package/dist/ui/Skills.d.ts.map +0 -1
  314. package/dist/ui/Skills.js.map +0 -1
  315. package/dist/ui/StackSelector.d.ts.map +0 -1
  316. package/dist/ui/StackSelector.js.map +0 -1
  317. package/dist/ui/Summary.d.ts.map +0 -1
  318. package/dist/ui/Summary.js.map +0 -1
  319. package/dist/ui/Welcome.d.ts.map +0 -1
  320. package/dist/ui/Welcome.js.map +0 -1
  321. package/dist/ui/theme.d.ts.map +0 -1
  322. package/dist/ui/theme.js.map +0 -1
@@ -1,32 +1,41 @@
1
- import { execFile } from 'child_process';
2
- import { promisify } from 'util';
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
- import { detectCIStack } from './ci.js';
6
- const execFileAsync = promisify(execFile);
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { execFileAsync } from "../lib/exec.js";
4
+ import { detectCIStack } from "./ci.js";
7
5
  // =============================================================================
8
6
  // Constants
9
7
  // =============================================================================
10
- const BASELINE_DIR = '.javi-forge';
11
- const BASELINE_FILE = 'security-baseline.json';
12
- const BASELINE_VERSION = '1.0.0';
8
+ const BASELINE_DIR = ".javi-forge";
9
+ const BASELINE_FILE = "security-baseline.json";
10
+ const BASELINE_VERSION = "2.0.0";
11
+ const SEVERITY_ORDER = {
12
+ critical: 5,
13
+ high: 4,
14
+ moderate: 3,
15
+ low: 2,
16
+ info: 1,
17
+ };
18
+ const DEFAULT_STALE_DAYS = 30;
13
19
  // =============================================================================
14
20
  // Audit command resolution
15
21
  // =============================================================================
16
22
  export function getAuditCommand(stack, buildTool) {
17
23
  switch (stack) {
18
- case 'node':
24
+ case "node":
19
25
  switch (buildTool) {
20
- case 'pnpm': return { cmd: 'pnpm', args: ['audit', '--json'] };
21
- case 'yarn': return { cmd: 'yarn', args: ['npm', 'audit', '--json'] };
22
- default: return { cmd: 'npm', args: ['audit', '--json'] };
26
+ case "pnpm":
27
+ return { cmd: "pnpm", args: ["audit", "--json"] };
28
+ case "yarn":
29
+ return { cmd: "yarn", args: ["npm", "audit", "--json"] };
30
+ default:
31
+ return { cmd: "npm", args: ["audit", "--json"] };
23
32
  }
24
- case 'python':
25
- return { cmd: 'pip-audit', args: ['--format=json', '--output=-'] };
26
- case 'go':
27
- return { cmd: 'govulncheck', args: ['-json', './...'] };
28
- case 'rust':
29
- return { cmd: 'cargo', args: ['audit', '--json'] };
33
+ case "python":
34
+ return { cmd: "pip-audit", args: ["--format=json", "--output=-"] };
35
+ case "go":
36
+ return { cmd: "govulncheck", args: ["-json", "./..."] };
37
+ case "rust":
38
+ return { cmd: "cargo", args: ["audit", "--json"] };
30
39
  default:
31
40
  return null;
32
41
  }
@@ -46,7 +55,7 @@ export function parseNpmAudit(raw) {
46
55
  for (const [pkgName, info] of Object.entries(vulns)) {
47
56
  const v = info;
48
57
  // via can contain objects (direct vulns) or strings (transitive refs)
49
- const directVias = (v.via ?? []).filter((x) => typeof x === 'object');
58
+ const directVias = (v.via ?? []).filter((x) => typeof x === "object");
50
59
  if (directVias.length === 0) {
51
60
  findings.push({
52
61
  id: `npm-${pkgName}`,
@@ -80,10 +89,10 @@ export function parsePipAudit(raw) {
80
89
  // pip-audit JSON: array of { name, version, vulns: [{ id, fix_versions, description }] }
81
90
  const deps = Array.isArray(data) ? data : (data.dependencies ?? []);
82
91
  for (const dep of deps) {
83
- for (const vuln of (dep.vulns ?? [])) {
92
+ for (const vuln of dep.vulns ?? []) {
84
93
  findings.push({
85
94
  id: vuln.id ?? `pip-${dep.name}`,
86
- severity: normalizeSeverity(vuln.fix_versions?.length ? 'high' : 'moderate'),
95
+ severity: normalizeSeverity(vuln.fix_versions?.length ? "high" : "moderate"),
87
96
  package: dep.name,
88
97
  title: vuln.description ?? `Vulnerability in ${dep.name}`,
89
98
  });
@@ -103,10 +112,10 @@ export function parseCargoAudit(raw) {
103
112
  for (const v of vulns) {
104
113
  const advisory = v.advisory ?? {};
105
114
  findings.push({
106
- id: advisory.id ?? `cargo-${v.package?.name ?? 'unknown'}`,
115
+ id: advisory.id ?? `cargo-${v.package?.name ?? "unknown"}`,
107
116
  severity: normalizeSeverity(advisory.cvss?.severity),
108
- package: v.package?.name ?? 'unknown',
109
- title: advisory.title ?? `Vulnerability in ${v.package?.name ?? 'unknown'}`,
117
+ package: v.package?.name ?? "unknown",
118
+ title: advisory.title ?? `Vulnerability in ${v.package?.name ?? "unknown"}`,
110
119
  url: advisory.url,
111
120
  });
112
121
  }
@@ -120,16 +129,16 @@ export function parseGovulncheck(raw) {
120
129
  const findings = [];
121
130
  try {
122
131
  // govulncheck JSON outputs one JSON object per line (NDJSON)
123
- const lines = raw.split('\n').filter(l => l.trim());
132
+ const lines = raw.split("\n").filter((l) => l.trim());
124
133
  for (const line of lines) {
125
134
  try {
126
135
  const entry = JSON.parse(line);
127
136
  if (entry.osv) {
128
137
  findings.push({
129
- id: entry.osv.id ?? 'unknown',
138
+ id: entry.osv.id ?? "unknown",
130
139
  severity: normalizeSeverity(entry.osv.database_specific?.severity),
131
- package: entry.osv.affected?.[0]?.package?.name ?? 'unknown',
132
- title: entry.osv.summary ?? entry.osv.id ?? 'Go vulnerability',
140
+ package: entry.osv.affected?.[0]?.package?.name ?? "unknown",
141
+ title: entry.osv.summary ?? entry.osv.id ?? "Go vulnerability",
133
142
  url: entry.osv.references?.[0]?.url,
134
143
  });
135
144
  }
@@ -146,39 +155,120 @@ export function parseGovulncheck(raw) {
146
155
  }
147
156
  function normalizeSeverity(raw) {
148
157
  if (!raw)
149
- return 'moderate';
158
+ return "moderate";
150
159
  const lower = raw.toLowerCase();
151
- if (lower === 'critical')
152
- return 'critical';
153
- if (lower === 'high')
154
- return 'high';
155
- if (lower === 'moderate' || lower === 'medium')
156
- return 'moderate';
157
- if (lower === 'low')
158
- return 'low';
159
- if (lower === 'info' || lower === 'none')
160
- return 'info';
161
- return 'moderate';
160
+ if (lower === "critical")
161
+ return "critical";
162
+ if (lower === "high")
163
+ return "high";
164
+ if (lower === "moderate" || lower === "medium")
165
+ return "moderate";
166
+ if (lower === "low")
167
+ return "low";
168
+ if (lower === "info" || lower === "none")
169
+ return "info";
170
+ return "moderate";
162
171
  }
163
172
  export function parseAuditOutput(stack, raw) {
164
173
  switch (stack) {
165
- case 'node': return parseNpmAudit(raw);
166
- case 'python': return parsePipAudit(raw);
167
- case 'rust': return parseCargoAudit(raw);
168
- case 'go': return parseGovulncheck(raw);
169
- default: return [];
174
+ case "node":
175
+ return parseNpmAudit(raw);
176
+ case "python":
177
+ return parsePipAudit(raw);
178
+ case "rust":
179
+ return parseCargoAudit(raw);
180
+ case "go":
181
+ return parseGovulncheck(raw);
182
+ default:
183
+ return [];
184
+ }
185
+ }
186
+ // =============================================================================
187
+ // Severity helpers
188
+ // =============================================================================
189
+ export function severityAtOrAbove(severity, threshold) {
190
+ return SEVERITY_ORDER[severity] >= SEVERITY_ORDER[threshold];
191
+ }
192
+ export function filterBySeverity(findings, minSeverity) {
193
+ return findings.filter((f) => severityAtOrAbove(f.severity, minSeverity));
194
+ }
195
+ // =============================================================================
196
+ // Allowlist filtering
197
+ // =============================================================================
198
+ export function filterAllowlisted(findings, allowlist) {
199
+ if (allowlist.length === 0)
200
+ return findings;
201
+ const allowSet = new Set(allowlist);
202
+ return findings.filter((f) => !allowSet.has(makeFindingKey(f)));
203
+ }
204
+ // =============================================================================
205
+ // Staleness detection
206
+ // =============================================================================
207
+ export function checkStaleness(baseline, staleDays) {
208
+ const refDate = baseline.updatedAt ?? baseline.createdAt;
209
+ const ageMs = Date.now() - new Date(refDate).getTime();
210
+ const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
211
+ if (ageDays > staleDays) {
212
+ return `Baseline is ${ageDays} days old (threshold: ${staleDays}). Consider running \`javi-forge security update\`.`;
213
+ }
214
+ return undefined;
215
+ }
216
+ export function baselineAgeDays(baseline) {
217
+ const refDate = baseline.updatedAt ?? baseline.createdAt;
218
+ const ageMs = Date.now() - new Date(refDate).getTime();
219
+ return Math.floor(ageMs / (1000 * 60 * 60 * 24));
220
+ }
221
+ // =============================================================================
222
+ // Summary computation
223
+ // =============================================================================
224
+ export function computeSummary(current, regressions, resolved, filteredRegressions, baseline) {
225
+ const bySeverity = {
226
+ critical: 0,
227
+ high: 0,
228
+ moderate: 0,
229
+ low: 0,
230
+ info: 0,
231
+ };
232
+ for (const f of current) {
233
+ bySeverity[f.severity]++;
170
234
  }
235
+ return {
236
+ total: current.length,
237
+ bySeverity,
238
+ regressionCount: regressions.length,
239
+ resolvedCount: resolved.length,
240
+ filteredCount: filteredRegressions.length,
241
+ baselineAge: baselineAgeDays(baseline),
242
+ };
171
243
  }
172
244
  // =============================================================================
173
245
  // Regression detection
174
246
  // =============================================================================
175
- export function detectRegressions(baseline, current) {
247
+ export function detectRegressions(baseline, current, options = {}) {
176
248
  const baselineKeySet = new Set(baseline.findingKeys);
177
249
  const currentKeys = current.map(makeFindingKey);
178
250
  const currentKeySet = new Set(currentKeys);
179
- const regressions = current.filter(f => !baselineKeySet.has(makeFindingKey(f)));
180
- const resolved = baseline.findings.filter(f => !currentKeySet.has(makeFindingKey(f)));
181
- return { baseline, current, regressions, resolved };
251
+ let regressions = current.filter((f) => !baselineKeySet.has(makeFindingKey(f)));
252
+ const resolved = baseline.findings.filter((f) => !currentKeySet.has(makeFindingKey(f)));
253
+ // Apply allowlist filtering
254
+ const allowlist = baseline.allowlist ?? [];
255
+ regressions = filterAllowlisted(regressions, allowlist);
256
+ // Apply severity threshold
257
+ const minSeverity = options.minSeverity ?? "low";
258
+ const filteredRegressions = filterBySeverity(regressions, minSeverity);
259
+ // Check staleness
260
+ const staleDays = options.staleDays ?? DEFAULT_STALE_DAYS;
261
+ const staleWarning = checkStaleness(baseline, staleDays);
262
+ const summary = computeSummary(current, regressions, resolved, filteredRegressions, baseline);
263
+ return {
264
+ baseline,
265
+ current,
266
+ regressions,
267
+ resolved,
268
+ filteredRegressions,
269
+ staleWarning,
270
+ summary,
271
+ };
182
272
  }
183
273
  // =============================================================================
184
274
  // Baseline file I/O
@@ -188,7 +278,7 @@ function baselinePath(projectDir) {
188
278
  }
189
279
  export async function readBaseline(projectDir) {
190
280
  const bp = baselinePath(projectDir);
191
- if (!await fs.pathExists(bp))
281
+ if (!(await fs.pathExists(bp)))
192
282
  return null;
193
283
  try {
194
284
  return await fs.readJson(bp);
@@ -200,6 +290,27 @@ export async function readBaseline(projectDir) {
200
290
  export async function writeBaseline(projectDir, baseline) {
201
291
  const bp = baselinePath(projectDir);
202
292
  await fs.ensureDir(path.dirname(bp));
293
+ // Refuse to follow symlinks: if .javi-forge/security-baseline.json has been
294
+ // replaced with a symlink (to /etc/passwd, /home/user/.ssh/authorized_keys,
295
+ // or any other sensitive file owned by the user) the default fs.writeJson
296
+ // would clobber the target. lstat reports the link, not what it points to.
297
+ let isSymlink = false;
298
+ try {
299
+ const stat = await fs.lstat(bp);
300
+ isSymlink = stat.isSymbolicLink();
301
+ }
302
+ catch (e) {
303
+ const code = e && typeof e === "object" && "code" in e
304
+ ? e.code
305
+ : "";
306
+ if (code !== "ENOENT") {
307
+ throw e;
308
+ }
309
+ // ENOENT is expected on first run; continue.
310
+ }
311
+ if (isSymlink) {
312
+ throw new Error(`Refusing to write baseline through a symlink: ${bp}. Remove the symlink first.`);
313
+ }
203
314
  await fs.writeJson(bp, baseline, { spaces: 2 });
204
315
  }
205
316
  // =============================================================================
@@ -217,7 +328,7 @@ async function runAuditTool(projectDir, auditCmd) {
217
328
  catch (err) {
218
329
  // npm audit exits non-zero when vulns are found — that's expected
219
330
  // We still want the stdout (JSON output)
220
- if (err && typeof err === 'object' && 'stdout' in err) {
331
+ if (err && typeof err === "object" && "stdout" in err) {
221
332
  const stdout = err.stdout;
222
333
  if (stdout && stdout.trim().length > 0)
223
334
  return stdout;
@@ -231,75 +342,123 @@ async function runAuditTool(projectDir, auditCmd) {
231
342
  function report(onStep, id, label, status, detail) {
232
343
  onStep({ id, label, status, detail });
233
344
  }
234
- export async function runSecurity(mode, projectDir, onStep) {
345
+ export async function runSecurity(mode, projectDir, onStep, options = {}) {
235
346
  // ── Detect stack ────────────────────────────────────────────────────────
236
- report(onStep, 'detect', 'Detecting stack', 'running');
347
+ report(onStep, "detect", "Detecting stack", "running");
237
348
  let stackInfo;
238
349
  try {
239
350
  stackInfo = await detectCIStack(projectDir);
240
- report(onStep, 'detect', `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, 'done');
351
+ report(onStep, "detect", `Stack: ${stackInfo.stackType} (${stackInfo.buildTool})`, "done");
241
352
  }
242
353
  catch (e) {
243
- report(onStep, 'detect', 'Detecting stack', 'error', String(e));
354
+ report(onStep, "detect", "Detecting stack", "error", String(e));
244
355
  throw e;
245
356
  }
246
357
  // ── Resolve audit command ──────────────────────────────────────────────
247
358
  const auditCmd = getAuditCommand(stackInfo.stackType, stackInfo.buildTool);
248
359
  if (!auditCmd) {
249
- report(onStep, 'audit', 'Security audit', 'error', `No audit tool for stack "${stackInfo.stackType}". Supported: node, python, go, rust`);
360
+ report(onStep, "audit", "Security audit", "error", `No audit tool for stack "${stackInfo.stackType}". Supported: node, python, go, rust`);
250
361
  throw new Error(`Unsupported stack for security audit: ${stackInfo.stackType}`);
251
362
  }
252
363
  // ── Run audit ──────────────────────────────────────────────────────────
253
- report(onStep, 'audit', `Running ${auditCmd.cmd} audit`, 'running');
364
+ report(onStep, "audit", `Running ${auditCmd.cmd} audit`, "running");
254
365
  let raw;
255
366
  try {
256
367
  raw = await runAuditTool(projectDir, auditCmd);
257
- report(onStep, 'audit', `Audit complete`, 'done');
368
+ report(onStep, "audit", `Audit complete`, "done");
258
369
  }
259
370
  catch (e) {
260
- report(onStep, 'audit', `Audit failed`, 'error', `${auditCmd.cmd} not found or failed. Install it first.`);
371
+ report(onStep, "audit", `Audit failed`, "error", `${auditCmd.cmd} not found or failed. Install it first.`);
261
372
  throw e;
262
373
  }
263
374
  // ── Parse findings ─────────────────────────────────────────────────────
264
375
  const findings = parseAuditOutput(stackInfo.stackType, raw);
265
376
  switch (mode) {
266
- case 'baseline':
267
- case 'update': {
268
- report(onStep, 'save', mode === 'update' ? 'Updating baseline' : 'Creating baseline', 'running');
377
+ case "baseline":
378
+ case "update": {
379
+ report(onStep, "save", mode === "update" ? "Updating baseline" : "Creating baseline", "running");
380
+ // Preserve createdAt and allowlist on update
381
+ let createdAt = new Date().toISOString();
382
+ let allowlist = [];
383
+ if (mode === "update") {
384
+ const existing = await readBaseline(projectDir);
385
+ if (existing) {
386
+ createdAt = existing.createdAt;
387
+ allowlist = existing.allowlist ?? [];
388
+ }
389
+ }
269
390
  const baseline = {
270
391
  version: BASELINE_VERSION,
271
- createdAt: new Date().toISOString(),
392
+ createdAt,
393
+ updatedAt: mode === "update" ? new Date().toISOString() : undefined,
272
394
  stack: stackInfo.stackType,
273
395
  buildTool: stackInfo.buildTool,
274
396
  findings,
275
397
  findingKeys: findings.map(makeFindingKey),
398
+ allowlist,
276
399
  };
277
400
  await writeBaseline(projectDir, baseline);
278
- report(onStep, 'save', `Baseline saved with ${findings.length} finding(s)`, 'done', baselinePath(projectDir));
401
+ report(onStep, "save", `Baseline saved with ${findings.length} finding(s)`, "done", baselinePath(projectDir));
279
402
  return null;
280
403
  }
281
- case 'check': {
282
- report(onStep, 'check', 'Checking for regressions', 'running');
404
+ case "check": {
405
+ report(onStep, "check", "Checking for regressions", "running");
283
406
  const existing = await readBaseline(projectDir);
284
407
  if (!existing) {
285
- report(onStep, 'check', 'No baseline found', 'error', 'Run `javi-forge security baseline` first to create a baseline');
286
- throw new Error('No security baseline found. Run `javi-forge security baseline` first.');
408
+ report(onStep, "check", "No baseline found", "error", "Run `javi-forge security baseline` first to create a baseline");
409
+ throw new Error("No security baseline found. Run `javi-forge security baseline` first.");
287
410
  }
288
- const result = detectRegressions(existing, findings);
289
- if (result.regressions.length === 0) {
411
+ const result = detectRegressions(existing, findings, options);
412
+ // Staleness warning
413
+ if (result.staleWarning) {
414
+ report(onStep, "stale", "Baseline staleness", "skipped", result.staleWarning);
415
+ }
416
+ // Summary line
417
+ const { summary } = result;
418
+ const sevBreakdown = Object.entries(summary.bySeverity)
419
+ .filter(([, count]) => count > 0)
420
+ .map(([sev, count]) => `${count} ${sev}`)
421
+ .join(", ");
422
+ if (sevBreakdown) {
423
+ report(onStep, "summary", `Current findings: ${summary.total} (${sevBreakdown})`, "done");
424
+ }
425
+ // Use filteredRegressions (severity-filtered + allowlist-filtered) for pass/fail
426
+ if (result.filteredRegressions.length === 0) {
290
427
  const resolvedMsg = result.resolved.length > 0
291
428
  ? ` (${result.resolved.length} resolved)`
292
- : '';
293
- report(onStep, 'check', `No new vulnerabilities${resolvedMsg}`, 'done');
429
+ : "";
430
+ const belowThreshold = result.regressions.length > result.filteredRegressions.length
431
+ ? ` (${result.regressions.length - result.filteredRegressions.length} below threshold)`
432
+ : "";
433
+ report(onStep, "check", `No actionable regressions${resolvedMsg}${belowThreshold}`, "done");
294
434
  }
295
435
  else {
296
- const details = result.regressions
297
- .map(r => ` ${r.severity.toUpperCase()} ${r.package}: ${r.title}`)
298
- .join('\n');
299
- report(onStep, 'check', `${result.regressions.length} regression(s) found`, 'error', details);
436
+ const details = result.filteredRegressions
437
+ .map((r) => ` ${r.severity.toUpperCase()} ${r.package}: ${r.title}`)
438
+ .join("\n");
439
+ report(onStep, "check", `${result.filteredRegressions.length} regression(s) found`, "error", details);
300
440
  }
301
441
  return result;
302
442
  }
443
+ case "allowlist": {
444
+ report(onStep, "allowlist", "Updating allowlist", "running");
445
+ const existing = await readBaseline(projectDir);
446
+ if (!existing) {
447
+ report(onStep, "allowlist", "No baseline found", "error", "Run `javi-forge security baseline` first to create a baseline");
448
+ throw new Error("No security baseline found. Run `javi-forge security baseline` first.");
449
+ }
450
+ // Add all current findings to the allowlist
451
+ const currentKeys = findings.map(makeFindingKey);
452
+ const existingAllowlist = new Set(existing.allowlist ?? []);
453
+ for (const key of currentKeys) {
454
+ existingAllowlist.add(key);
455
+ }
456
+ existing.allowlist = [...existingAllowlist];
457
+ existing.updatedAt = new Date().toISOString();
458
+ await writeBaseline(projectDir, existing);
459
+ report(onStep, "allowlist", `Allowlist updated: ${existingAllowlist.size} finding(s) allowed`, "done", baselinePath(projectDir));
460
+ return null;
461
+ }
303
462
  }
304
463
  }
305
464
  //# sourceMappingURL=security.js.map
@@ -0,0 +1,19 @@
1
+ import type { ConflictKind, SkillBudgetEntry, SkillBudgetResult, SkillBudgetSuggestion, SkillConflict, SkillDuplicate } from "../../types/index.js";
2
+ /**
3
+ * Generate minimal disable sets to bring total tokens under budget.
4
+ * Uses a greedy approach: disable largest skills first until under budget.
5
+ * Returns up to 3 alternative optimization suggestions.
6
+ */
7
+ export declare function generateBudgetOptimizations(entries: SkillBudgetEntry[], totalTokens: number, budget: number): SkillBudgetSuggestion[];
8
+ /** Check if two rules contradict each other (regex pairs + directive clash) */
9
+ export declare function detectRuleConflict(ruleA: string, ruleB: string): {
10
+ reason: string;
11
+ kind: ConflictKind;
12
+ } | null;
13
+ /** Scan all skills for conflicting critical rules */
14
+ export declare function findConflicts(skillsDir: string): Promise<SkillConflict[]>;
15
+ /** Calculate token budget for all installed skills */
16
+ export declare function calculateBudget(skillsDir: string, budget?: number): Promise<SkillBudgetResult>;
17
+ /** Find skills that overlap in scope/triggers */
18
+ export declare function findDuplicates(skillsDir: string): Promise<SkillDuplicate[]>;
19
+ //# sourceMappingURL=analysis.d.ts.map