mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-07-28 23:23:48 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
mode: atomic
|
||||
github.com/yourorg/devour/cmd/devour/main.go:7.13,9.2 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:11.13,38.30 7 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:38.30,42.15 3 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:42.15,44.12 2 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:47.3,56.17 4 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:56.17,58.12 2 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:61.3,63.20 2 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:63.20,68.32 5 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:68.32,70.5 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:70.10,72.5 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:75.3,76.17 2 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:76.17,78.4 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:78.9,80.4 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:82.3,82.16 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:85.2,108.30 5 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:108.30,110.15 2 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:110.15,112.4 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:112.9,114.4 1 0
|
||||
github.com/yourorg/devour/cmd/realtest/main.go:117.2,118.38 2 0
|
||||
github.com/yourorg/devour/cmd/demo.go:26.13,28.2 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:30.55,37.51 5 0
|
||||
github.com/yourorg/devour/cmd/demo.go:37.51,39.3 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:41.2,51.16 7 0
|
||||
github.com/yourorg/devour/cmd/demo.go:51.16,53.3 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:55.2,60.16 4 0
|
||||
github.com/yourorg/devour/cmd/demo.go:60.16,62.3 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:64.2,68.29 3 0
|
||||
github.com/yourorg/devour/cmd/demo.go:68.29,69.20 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:69.20,71.18 2 0
|
||||
github.com/yourorg/devour/cmd/demo.go:71.18,73.5 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:76.2,83.29 5 0
|
||||
github.com/yourorg/devour/cmd/demo.go:83.29,84.58 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:84.58,86.9 2 0
|
||||
github.com/yourorg/devour/cmd/demo.go:90.2,90.21 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:90.21,92.17 2 0
|
||||
github.com/yourorg/devour/cmd/demo.go:92.17,97.31 4 0
|
||||
github.com/yourorg/devour/cmd/demo.go:97.31,98.16 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:98.16,100.11 2 0
|
||||
github.com/yourorg/devour/cmd/demo.go:102.5,102.32 1 0
|
||||
github.com/yourorg/devour/cmd/demo.go:107.2,124.12 17 0
|
||||
github.com/yourorg/devour/cmd/get.go:40.13,45.2 3 0
|
||||
github.com/yourorg/devour/cmd/get.go:47.54,53.16 4 0
|
||||
github.com/yourorg/devour/cmd/get.go:53.16,55.3 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:58.2,70.44 8 0
|
||||
github.com/yourorg/devour/cmd/get.go:73.64,74.18 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:75.22,76.60 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:77.14,78.77 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:79.22,80.51 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:80.51,82.4 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:83.3,83.80 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:84.14,85.88 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:86.16,87.111 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:88.26,89.91 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:90.15,91.75 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:92.13,93.70 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:94.14,95.69 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:96.16,97.65 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:98.26,99.75 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:100.15,101.76 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:102.10,103.172 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:107.48,108.18 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:109.22,110.18 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:111.14,112.20 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:113.22,114.22 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:115.14,116.20 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:117.16,118.22 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:119.26,120.18 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:121.15,122.21 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:123.13,124.19 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:125.14,126.20 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:127.16,128.22 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:129.26,130.26 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:131.15,132.21 1 0
|
||||
github.com/yourorg/devour/cmd/get.go:133.10,134.15 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:31.13,34.2 2 0
|
||||
github.com/yourorg/devour/cmd/init.go:36.55,39.19 2 0
|
||||
github.com/yourorg/devour/cmd/init.go:39.19,41.3 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:44.2,44.53 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:44.53,46.3 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:48.2,51.61 2 0
|
||||
github.com/yourorg/devour/cmd/init.go:51.61,53.3 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:56.2,57.71 2 0
|
||||
github.com/yourorg/devour/cmd/init.go:57.71,59.3 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:62.2,70.27 3 0
|
||||
github.com/yourorg/devour/cmd/init.go:70.27,71.48 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:71.48,73.4 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:76.2,83.12 7 0
|
||||
github.com/yourorg/devour/cmd/init.go:86.48,88.12 2 0
|
||||
github.com/yourorg/devour/cmd/init.go:88.12,90.3 1 0
|
||||
github.com/yourorg/devour/cmd/init.go:92.2,143.9 1 0
|
||||
github.com/yourorg/devour/cmd/languages.go:21.13,23.2 1 0
|
||||
github.com/yourorg/devour/cmd/languages.go:25.60,97.33 5 0
|
||||
github.com/yourorg/devour/cmd/languages.go:97.33,101.41 4 0
|
||||
github.com/yourorg/devour/cmd/languages.go:101.41,103.4 1 0
|
||||
github.com/yourorg/devour/cmd/languages.go:104.3,104.16 1 0
|
||||
github.com/yourorg/devour/cmd/languages.go:107.2,117.12 10 0
|
||||
github.com/yourorg/devour/cmd/push.go:32.13,35.2 2 0
|
||||
github.com/yourorg/devour/cmd/push.go:37.55,40.22 2 0
|
||||
github.com/yourorg/devour/cmd/push.go:40.22,43.3 1 0
|
||||
github.com/yourorg/devour/cmd/push.go:45.2,47.23 3 0
|
||||
github.com/yourorg/devour/cmd/push.go:47.23,49.3 1 0
|
||||
github.com/yourorg/devour/cmd/push.go:57.2,61.12 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:136.13,171.2 22 0
|
||||
github.com/yourorg/devour/cmd/quality.go:173.62,175.19 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:175.19,177.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:179.2,179.49 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:179.49,181.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:183.2,200.16 6 0
|
||||
github.com/yourorg/devour/cmd/quality.go:200.16,203.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:205.2,206.8 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:206.8,208.59 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:208.59,210.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:211.8,215.3 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:217.2,219.16 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:219.16,221.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:223.2,223.48 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:226.64,234.54 5 0
|
||||
github.com/yourorg/devour/cmd/quality.go:234.54,239.55 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:239.55,242.4 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:245.2,245.24 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:245.24,248.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:251.2,255.23 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:256.14,257.54 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:258.10,260.13 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:264.62,271.54 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:271.54,275.55 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:275.55,277.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:280.2,280.24 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:280.24,283.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:286.2,289.17 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:289.17,292.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:295.2,295.14 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:295.14,296.33 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:296.33,298.37 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:298.37,299.78 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:299.78,301.11 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:304.4,304.49 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:304.49,307.5 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:312.2,320.13 8 0
|
||||
github.com/yourorg/devour/cmd/quality.go:320.13,323.24 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:324.27,325.90 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:326.27,327.94 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:328.27,329.97 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:330.27,331.93 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:334.3,334.104 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:337.2,337.12 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:340.65,341.19 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:341.19,343.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:345.2,356.28 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:356.28,358.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:360.2,360.23 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:360.23,362.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:365.2,370.54 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:370.54,374.59 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:374.59,376.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:380.2,381.35 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:381.35,382.23 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:382.23,385.31 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:385.31,387.5 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:388.4,389.20 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:389.20,391.5 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:392.4,393.9 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:397.2,397.12 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:397.12,399.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:402.2,402.51 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:402.51,404.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:406.2,415.16 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:415.16,417.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:419.2,419.61 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:419.61,421.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:423.2,424.23 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:424.23,426.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:428.2,428.12 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:431.72,434.51 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:434.51,436.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:438.2,448.16 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:448.16,450.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:452.2,452.61 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:452.61,454.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:457.2,457.16 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:458.14,459.51 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:460.10,461.38 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:465.61,473.31 7 0
|
||||
github.com/yourorg/devour/cmd/quality.go:473.31,476.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:478.2,479.42 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:479.42,481.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:483.2,491.33 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:491.33,493.25 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:493.25,494.12 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:497.3,498.36 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:498.36,501.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:502.3,502.16 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:505.2,508.12 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:511.61,516.54 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:516.54,520.55 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:520.55,522.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:525.2,525.24 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:525.24,528.3 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:530.2,539.12 5 0
|
||||
github.com/yourorg/devour/cmd/quality.go:539.12,540.36 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:540.36,541.86 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:541.86,542.13 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:545.4,545.42 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:545.42,546.30 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:546.30,548.20 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:548.20,550.15 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:552.6,552.24 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:552.24,555.7 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:556.6,556.11 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:560.8,561.20 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:561.20,563.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:565.3,567.27 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:567.27,568.34 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:568.34,570.10 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:574.3,574.20 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:574.20,576.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:578.3,578.41 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:578.41,579.29 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:579.29,581.19 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:581.19,583.6 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:584.5,586.10 3 0
|
||||
github.com/yourorg/devour/cmd/quality.go:591.2,591.15 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:591.15,593.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:593.8,595.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:597.2,597.21 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:597.21,599.28 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:599.28,601.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:604.2,604.12 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:607.64,610.19 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:610.19,612.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:614.2,614.24 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:614.24,616.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:618.2,618.103 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:621.48,627.54 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:627.54,632.55 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:632.55,635.4 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:638.2,643.16 5 0
|
||||
github.com/yourorg/devour/cmd/quality.go:643.16,645.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:647.2,648.51 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:648.51,650.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:652.2,656.12 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:659.67,665.16 4 0
|
||||
github.com/yourorg/devour/cmd/quality.go:665.16,667.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:669.2,672.56 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:672.56,674.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:674.8,676.64 2 0
|
||||
github.com/yourorg/devour/cmd/quality.go:676.64,678.4 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:679.3,679.30 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:682.2,682.77 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:682.77,684.3 1 0
|
||||
github.com/yourorg/devour/cmd/quality.go:686.2,688.12 2 0
|
||||
github.com/yourorg/devour/cmd/query.go:31.13,35.2 3 0
|
||||
github.com/yourorg/devour/cmd/query.go:37.56,39.19 2 0
|
||||
github.com/yourorg/devour/cmd/query.go:39.19,41.3 1 0
|
||||
github.com/yourorg/devour/cmd/query.go:43.2,59.12 9 0
|
||||
github.com/yourorg/devour/cmd/root.go:29.16,30.42 1 0
|
||||
github.com/yourorg/devour/cmd/root.go:30.42,33.3 2 0
|
||||
github.com/yourorg/devour/cmd/root.go:36.13,52.2 13 0
|
||||
github.com/yourorg/devour/cmd/root.go:54.19,55.19 1 0
|
||||
github.com/yourorg/devour/cmd/root.go:55.19,57.3 1 0
|
||||
github.com/yourorg/devour/cmd/root.go:57.8,62.3 4 0
|
||||
github.com/yourorg/devour/cmd/root.go:64.2,67.56 3 0
|
||||
github.com/yourorg/devour/cmd/root.go:67.56,69.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:57.13,63.2 5 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:65.57,66.25 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:66.25,68.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:70.2,70.20 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:70.20,72.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:74.2,85.22 4 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:85.22,87.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:89.2,95.14 6 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:95.14,97.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:99.2,109.16 5 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:109.16,111.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:113.2,115.24 2 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:115.24,117.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:119.2,119.56 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:119.56,121.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:123.2,123.27 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:123.27,127.33 3 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:127.33,145.4 4 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:145.9,148.18 3 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:148.18,150.5 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:153.3,154.63 2 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:154.63,156.4 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:158.3,158.53 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:161.2,165.12 4 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:168.48,170.2 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:172.60,174.16 2 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:174.16,176.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:178.2,181.9 3 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:182.69,183.34 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:184.56,185.36 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:186.33,187.38 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:188.33,189.36 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:190.32,191.38 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:192.72,193.34 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:194.27,195.37 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:196.27,197.35 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:198.26,199.36 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:200.73,201.38 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:201.38,203.4 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:204.3,204.38 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:205.43,206.42 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:207.34,208.37 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:209.28,210.34 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:211.10,212.31 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:216.43,218.16 2 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:218.16,220.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:222.2,223.20 2 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:223.20,225.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:227.2,227.15 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:230.43,237.20 6 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:237.20,239.3 1 0
|
||||
github.com/yourorg/devour/cmd/scrape.go:241.2,241.13 1 0
|
||||
github.com/yourorg/devour/cmd/serve.go:33.13,37.2 3 0
|
||||
github.com/yourorg/devour/cmd/serve.go:39.56,40.17 1 0
|
||||
github.com/yourorg/devour/cmd/serve.go:40.17,48.3 5 0
|
||||
github.com/yourorg/devour/cmd/serve.go:50.2,60.53 3 0
|
||||
github.com/yourorg/devour/cmd/status.go:24.57,54.2 16 0
|
||||
github.com/yourorg/devour/cmd/sync.go:30.13,34.2 3 0
|
||||
github.com/yourorg/devour/cmd/sync.go:36.55,37.17 1 0
|
||||
github.com/yourorg/devour/cmd/sync.go:37.17,39.3 1 0
|
||||
github.com/yourorg/devour/cmd/sync.go:39.8,41.3 1 0
|
||||
github.com/yourorg/devour/cmd/sync.go:43.2,43.22 1 0
|
||||
github.com/yourorg/devour/cmd/sync.go:43.22,45.3 1 0
|
||||
github.com/yourorg/devour/cmd/sync.go:57.2,61.12 4 0
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var demoCmd = &cobra.Command{
|
||||
Use: "demo",
|
||||
Short: "Run a demonstration of Devour features",
|
||||
Long: `Demonstrates Devour's capabilities by scraping sample documentation
|
||||
and showing the enhanced markdown output.
|
||||
|
||||
This command will:
|
||||
1. Scrape Go's HTTP package documentation
|
||||
2. Generate enhanced markdown output
|
||||
3. Display a preview of the results
|
||||
4. Show statistics about the operation`,
|
||||
RunE: runDemo,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(demoCmd)
|
||||
}
|
||||
|
||||
func runDemo(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("🎬 Devour Feature Demonstration")
|
||||
fmt.Println("═══════════════════════════════════════════════════════════════")
|
||||
fmt.Println()
|
||||
|
||||
// Create demo directory
|
||||
demoDir := "devour_demo"
|
||||
if err := os.MkdirAll(demoDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create demo directory: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("📍 Step 1: Scraping Go HTTP package documentation...")
|
||||
fmt.Println(" Command: devour get go http --format markdown --output devour_demo")
|
||||
fmt.Println()
|
||||
|
||||
// Set demo output directory and format
|
||||
scrapeOutput = demoDir
|
||||
scrapeFormat = "markdown"
|
||||
|
||||
// Use the get command logic to fetch Go HTTP docs
|
||||
err := runGet(cmd, []string{"go", "http"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("demo scrape failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("📊 Step 2: Analyzing results...")
|
||||
|
||||
// Count files in demo directory
|
||||
files, err := os.ReadDir(demoDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read demo directory: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Generated %d markdown files\n", len(files))
|
||||
|
||||
// Show file sizes
|
||||
var totalSize int64
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
info, err := file.Info()
|
||||
if err == nil {
|
||||
totalSize += info.Size()
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf(" ✓ Total size: %d bytes\n", totalSize)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("📖 Step 3: Preview of enhanced markdown output...")
|
||||
|
||||
// Find and show preview of first markdown file
|
||||
var firstFile string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".md" {
|
||||
firstFile = filepath.Join(demoDir, file.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if firstFile != "" {
|
||||
content, err := os.ReadFile(firstFile)
|
||||
if err == nil {
|
||||
// Show first 20 lines
|
||||
lines := strings.Split(string(content), "\n")
|
||||
fmt.Printf(" From file: %s\n", filepath.Base(firstFile))
|
||||
fmt.Println(" ─────────────────────────────────────────")
|
||||
for i, line := range lines {
|
||||
if i >= 20 {
|
||||
fmt.Printf(" ... (%d more lines)\n", len(lines)-20)
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("✨ Demo Complete!")
|
||||
fmt.Println()
|
||||
fmt.Println("🚀 What you can do next:")
|
||||
fmt.Printf(" • View all files: ls -la %s/\n", demoDir)
|
||||
fmt.Printf(" • Open in editor: code %s/\n", demoDir)
|
||||
fmt.Println(" • Try other languages: devour get python asyncio")
|
||||
fmt.Println(" • Use enhanced format: devour get react hooks --format markdown")
|
||||
fmt.Println(" • See all languages: devour languages")
|
||||
fmt.Println()
|
||||
fmt.Println("💡 Pro Tip: The enhanced markdown includes:")
|
||||
fmt.Println(" • 📋 Document metadata tables")
|
||||
fmt.Println(" • 📑 Auto-generated table of contents")
|
||||
fmt.Println(" • 🎨 Visual indicators for important content")
|
||||
fmt.Println(" • 🔗 Automatic link conversion")
|
||||
fmt.Println(" • 📚 Structured content sections")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/yourorg/devour/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <language> <keyword>",
|
||||
Short: "Get documentation for a language/framework",
|
||||
Long: `Quickly fetch documentation for popular languages and frameworks.
|
||||
This command automatically maps language+keyword combinations to their official documentation sites.
|
||||
|
||||
Supported languages:
|
||||
go, golang - Go documentation (pkg.go.dev)
|
||||
rust - Rust documentation (docs.rs)
|
||||
python, py - Python documentation (docs.python.org)
|
||||
java - Java documentation (docs.oracle.com)
|
||||
spring - Spring Boot documentation (docs.spring.io)
|
||||
typescript, ts - TypeScript documentation (typescriptlang.org)
|
||||
react - React documentation (react.dev)
|
||||
vue - Vue.js documentation (vuejs.org)
|
||||
nuxt - Nuxt documentation (nuxt.com)
|
||||
docker - Docker documentation (docs.docker.com)
|
||||
cloudflare, cf - Cloudflare documentation (developers.cloudflare.com)
|
||||
astro - Astro documentation (docs.astro.build)
|
||||
|
||||
Examples:
|
||||
devour get go http # Go HTTP package documentation
|
||||
devour get python asyncio # Python asyncio module
|
||||
devour get react hooks # React Hooks documentation
|
||||
devour get docker compose # Docker Compose docs
|
||||
devour get rust tokio # Rust Tokio crate`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runGet,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add flags that can override defaults
|
||||
getCmd.Flags().StringVarP(&scrapeFormat, "format", "f", "json", "output format (json, markdown)")
|
||||
getCmd.Flags().StringVarP(&scrapeOutput, "output", "o", "", "output directory (default: devour_data/docs)")
|
||||
getCmd.Flags().IntVar(&scrapeConcurrency, "concurrency", 10, "parallel scraping workers")
|
||||
}
|
||||
|
||||
func runGet(cmd *cobra.Command, args []string) error {
|
||||
language := strings.ToLower(args[0])
|
||||
keyword := strings.ToLower(args[1])
|
||||
|
||||
// Map language to base URL and construct full URL
|
||||
url, err := constructDocURL(language, keyword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the scrape type based on language
|
||||
sourceType := mapLanguageToType(language)
|
||||
|
||||
// Reuse the existing scrape logic with pre-determined values
|
||||
scrapeType = string(sourceType)
|
||||
sourceURL := url
|
||||
|
||||
fmt.Printf("Getting docs for: %s %s\n", language, keyword)
|
||||
fmt.Printf("URL: %s\n", sourceURL)
|
||||
fmt.Printf("Type: %s\n", sourceType)
|
||||
fmt.Println()
|
||||
|
||||
// Call the existing scrape logic
|
||||
return runScrape(cmd, []string{sourceURL})
|
||||
}
|
||||
|
||||
func constructDocURL(language, keyword string) (string, error) {
|
||||
switch language {
|
||||
case "go", "golang":
|
||||
return fmt.Sprintf("https://pkg.go.dev/%s", keyword), nil
|
||||
case "rust":
|
||||
return fmt.Sprintf("https://docs.rs/%s/latest/%s/", keyword, keyword), nil
|
||||
case "python", "py":
|
||||
if keyword == "stdlib" || keyword == "standard" {
|
||||
return "https://docs.python.org/3/library/", nil
|
||||
}
|
||||
return fmt.Sprintf("https://docs.python.org/3/library/%s.html", keyword), nil
|
||||
case "java":
|
||||
return fmt.Sprintf("https://docs.oracle.com/javase/8/docs/api/%s.html", keyword), nil
|
||||
case "spring":
|
||||
return fmt.Sprintf("https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#%s", keyword), nil
|
||||
case "typescript", "ts":
|
||||
return fmt.Sprintf("https://www.typescriptlang.org/docs/handbook/%s.html", keyword), nil
|
||||
case "react":
|
||||
return fmt.Sprintf("https://react.dev/reference/react/%s", keyword), nil
|
||||
case "vue":
|
||||
return fmt.Sprintf("https://vuejs.org/guide/%s.html", keyword), nil
|
||||
case "nuxt":
|
||||
return fmt.Sprintf("https://nuxt.com/docs/guide/%s", keyword), nil
|
||||
case "docker":
|
||||
return fmt.Sprintf("https://docs.docker.com/%s", keyword), nil
|
||||
case "cloudflare", "cf":
|
||||
return fmt.Sprintf("https://developers.cloudflare.com/%s", keyword), nil
|
||||
case "astro":
|
||||
return fmt.Sprintf("https://docs.astro.build/en/guides/%s", keyword), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported language: %s. Supported languages: go, rust, python, java, spring, typescript, react, vue, nuxt, docker, cloudflare, astro", language)
|
||||
}
|
||||
}
|
||||
|
||||
func mapLanguageToType(language string) string {
|
||||
switch language {
|
||||
case "go", "golang":
|
||||
return "godocs"
|
||||
case "rust":
|
||||
return "rustdocs"
|
||||
case "python", "py":
|
||||
return "pythondocs"
|
||||
case "java":
|
||||
return "javadocs"
|
||||
case "spring":
|
||||
return "springdocs"
|
||||
case "typescript", "ts":
|
||||
return "tsdocs"
|
||||
case "react":
|
||||
return "reactdocs"
|
||||
case "vue":
|
||||
return "vuedocs"
|
||||
case "nuxt":
|
||||
return "nuxtdocs"
|
||||
case "docker":
|
||||
return "dockerdocs"
|
||||
case "cloudflare", "cf":
|
||||
return "cloudflaredocs"
|
||||
case "astro":
|
||||
return "astrodocs"
|
||||
default:
|
||||
return "web"
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init [path]",
|
||||
Short: "Initialize Devour for a project",
|
||||
Long: `Initialize Devour configuration for a project.
|
||||
|
||||
Creates a devour.yaml configuration file and the necessary directory
|
||||
structure for storing documents and indexes.
|
||||
|
||||
Examples:
|
||||
devour init # Initialize in current directory
|
||||
devour init ./my-project # Initialize in specific directory
|
||||
devour init --remote # Initialize with remote mode config`,
|
||||
RunE: runInit,
|
||||
}
|
||||
|
||||
var (
|
||||
initRemote bool
|
||||
initForce bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCmd.Flags().BoolVar(&initRemote, "remote", false, "configure for remote mode")
|
||||
initCmd.Flags().BoolVarP(&initForce, "force", "f", false, "overwrite existing config")
|
||||
}
|
||||
|
||||
func runInit(cmd *cobra.Command, args []string) error {
|
||||
// Determine target directory
|
||||
targetDir := "."
|
||||
if len(args) > 0 {
|
||||
targetDir = args[0]
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(targetDir, "devour.yaml")
|
||||
|
||||
// Check if config already exists
|
||||
if _, err := os.Stat(configPath); err == nil && !initForce {
|
||||
return fmt.Errorf("config file already exists at %s (use --force to overwrite)", configPath)
|
||||
}
|
||||
|
||||
// Create default config
|
||||
config := generateDefaultConfig(initRemote)
|
||||
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
// Create data directories
|
||||
dataDir := filepath.Join(targetDir, "devour_data")
|
||||
dirs := []string{
|
||||
filepath.Join(dataDir, "docs"),
|
||||
filepath.Join(dataDir, "index"),
|
||||
filepath.Join(dataDir, "metadata"),
|
||||
filepath.Join(dataDir, "cache"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Created config at %s\n", configPath)
|
||||
fmt.Printf("✓ Created data directories at %s\n", dataDir)
|
||||
fmt.Println("\nNext steps:")
|
||||
fmt.Println(" 1. Set OPENAI_API_KEY environment variable")
|
||||
fmt.Println(" 2. Add sources to devour.yaml")
|
||||
fmt.Println(" 3. Run 'devour scrape <source>' to index documentation")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateDefaultConfig(remote bool) string {
|
||||
mode := "local"
|
||||
if remote {
|
||||
mode = "remote"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`# Devour Configuration
|
||||
version: 1
|
||||
|
||||
# Storage paths
|
||||
storage:
|
||||
docs_dir: ./devour_data/docs
|
||||
index_dir: ./devour_data/index
|
||||
metadata_dir: ./devour_data/metadata
|
||||
|
||||
# Embedding settings
|
||||
embeddings:
|
||||
provider: openai
|
||||
model: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
batch_size: 100
|
||||
|
||||
# Vector database
|
||||
vector_db:
|
||||
type: chromem
|
||||
persist: true
|
||||
similarity_metric: cosine
|
||||
|
||||
# Scraping settings
|
||||
scraper:
|
||||
user_agent: "Devour/1.0"
|
||||
timeout: 30s
|
||||
retry_count: 3
|
||||
concurrency: 10
|
||||
rate_limit: 500ms
|
||||
max_depth: 3
|
||||
cache_dir: ./devour_data/cache
|
||||
|
||||
# Scheduler
|
||||
scheduler:
|
||||
enabled: true
|
||||
interval: 72h
|
||||
check_method: hash
|
||||
|
||||
# Server settings
|
||||
server:
|
||||
mode: %s
|
||||
port: 8080
|
||||
host: localhost
|
||||
|
||||
# Sources (add your own)
|
||||
sources: []
|
||||
# - name: example-docs
|
||||
# type: url
|
||||
# url: https://docs.example.com
|
||||
# include: ["**/*.md", "**/*.html"]
|
||||
`, mode)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var languagesCmd = &cobra.Command{
|
||||
Use: "languages",
|
||||
Short: "Show supported languages and their mappings",
|
||||
Long: `Display all supported languages for the 'devour get' command
|
||||
along with their base URLs and examples.
|
||||
|
||||
This helps you discover what documentation sources are available
|
||||
and how to reference them quickly.`,
|
||||
RunE: runLanguages,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(languagesCmd)
|
||||
}
|
||||
|
||||
func runLanguages(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("🌐 Devour Supported Languages")
|
||||
fmt.Println("═══════════════════════════════════════════════════════════════")
|
||||
fmt.Println()
|
||||
|
||||
languages := []struct {
|
||||
langs []string
|
||||
url string
|
||||
examples []string
|
||||
}{
|
||||
{
|
||||
langs: []string{"go", "golang"},
|
||||
url: "https://pkg.go.dev/{package}",
|
||||
examples: []string{"devour get go http", "devour get go fmt", "devour get golang json"},
|
||||
},
|
||||
{
|
||||
langs: []string{"rust"},
|
||||
url: "https://docs.rs/{crate}/latest/{crate}/",
|
||||
examples: []string{"devour get rust tokio", "devour get rust serde", "devour get rust clap"},
|
||||
},
|
||||
{
|
||||
langs: []string{"python", "py"},
|
||||
url: "https://docs.python.org/3/library/{module}.html",
|
||||
examples: []string{"devour get python asyncio", "devour get py requests", "devour get python stdlib"},
|
||||
},
|
||||
{
|
||||
langs: []string{"java"},
|
||||
url: "https://docs.oracle.com/javase/8/docs/api/{package}.html",
|
||||
examples: []string{"devour get java string", "devour get java arraylist"},
|
||||
},
|
||||
{
|
||||
langs: []string{"spring"},
|
||||
url: "https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#{section}",
|
||||
examples: []string{"devour get spring boot", "devour get spring testing"},
|
||||
},
|
||||
{
|
||||
langs: []string{"typescript", "ts"},
|
||||
url: "https://www.typescriptlang.org/docs/handbook/{topic}.html",
|
||||
examples: []string{"devour get typescript interfaces", "devour get ts decorators"},
|
||||
},
|
||||
{
|
||||
langs: []string{"react"},
|
||||
url: "https://react.dev/reference/react/{feature}",
|
||||
examples: []string{"devour get react hooks", "devour get react components", "devour get react state"},
|
||||
},
|
||||
{
|
||||
langs: []string{"vue"},
|
||||
url: "https://vuejs.org/guide/{topic}.html",
|
||||
examples: []string{"devour get vue components", "devour get vue reactivity"},
|
||||
},
|
||||
{
|
||||
langs: []string{"nuxt"},
|
||||
url: "https://nuxt.com/docs/guide/{topic}",
|
||||
examples: []string{"devour get nuxt routing", "devour get nuxt middleware"},
|
||||
},
|
||||
{
|
||||
langs: []string{"docker"},
|
||||
url: "https://docs.docker.com/{topic}",
|
||||
examples: []string{"devour get docker compose", "devour get docker build", "devour get docker networking"},
|
||||
},
|
||||
{
|
||||
langs: []string{"cloudflare", "cf"},
|
||||
url: "https://developers.cloudflare.com/{topic}",
|
||||
examples: []string{"devour get cloudflare workers", "devour get cf pages", "devour get cloudflare dns"},
|
||||
},
|
||||
{
|
||||
langs: []string{"astro"},
|
||||
url: "https://docs.astro.build/en/guides/{topic}",
|
||||
examples: []string{"devour get astro routing", "devour get astro components"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, lang := range languages {
|
||||
fmt.Printf("🔷 %s\n", strings.Join(lang.langs, ", "))
|
||||
fmt.Printf(" URL: %s\n", lang.url)
|
||||
fmt.Printf(" Examples:\n")
|
||||
for _, example := range lang.examples {
|
||||
fmt.Printf(" • %s\n", example)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("💡 Pro Tips:")
|
||||
fmt.Println(" • Use 'devour get <language> help' for language-specific help")
|
||||
fmt.Println(" • Add --format markdown for enhanced documentation")
|
||||
fmt.Println(" • Most languages support common aliases (e.g., py → python)")
|
||||
fmt.Println()
|
||||
fmt.Println("🚀 Quick Start:")
|
||||
fmt.Println(" devour get go http --format markdown")
|
||||
fmt.Println(" devour get python asyncio")
|
||||
fmt.Println(" devour get react hooks")
|
||||
|
||||
return nil
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push <path>",
|
||||
Short: "Push documents to remote MCP server",
|
||||
Long: `Push local documents to a remote Devour MCP server.
|
||||
|
||||
Useful for:
|
||||
- Syncing local documentation to a shared server
|
||||
- Backing up indexed content
|
||||
- Contributing to a team knowledge base
|
||||
|
||||
Examples:
|
||||
devour push ./docs
|
||||
devour push ./docs --server http://devour.company.com
|
||||
devour push ./docs --server http://localhost:8080 --project my-project`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runPush,
|
||||
}
|
||||
|
||||
var (
|
||||
pushServer string
|
||||
pushProject string
|
||||
)
|
||||
|
||||
func init() {
|
||||
pushCmd.Flags().StringVar(&pushServer, "server", "", "remote Devour server URL")
|
||||
pushCmd.Flags().StringVarP(&pushProject, "project", "p", "", "project name on remote server")
|
||||
}
|
||||
|
||||
func runPush(cmd *cobra.Command, args []string) error {
|
||||
path := args[0]
|
||||
|
||||
if pushServer == "" {
|
||||
// Try to get from config
|
||||
pushServer = "http://localhost:8080"
|
||||
}
|
||||
|
||||
fmt.Printf("📤 Pushing to: %s\n", pushServer)
|
||||
fmt.Printf(" Path: %s\n", path)
|
||||
if pushProject != "" {
|
||||
fmt.Printf(" Project: %s\n", pushProject)
|
||||
}
|
||||
|
||||
// TODO: Implement actual push logic
|
||||
// 1. Scan path for documents
|
||||
// 2. Connect to remote server
|
||||
// 3. Upload documents
|
||||
// 4. Wait for indexing confirmation
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("⚠️ Push functionality not yet implemented")
|
||||
fmt.Println(" Remote server support coming soon")
|
||||
|
||||
return nil
|
||||
}
|
||||
+736
@@ -0,0 +1,736 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yourorg/devour/internal/quality"
|
||||
"github.com/yourorg/devour/internal/quality/detectors"
|
||||
"github.com/yourorg/devour/internal/quality/plugins"
|
||||
"github.com/yourorg/devour/internal/quality/plugins/go/fixers"
|
||||
"github.com/yourorg/devour/internal/quality/review"
|
||||
"github.com/yourorg/devour/internal/quality/scorecard"
|
||||
|
||||
_ "github.com/yourorg/devour/internal/quality/plugins/go"
|
||||
)
|
||||
|
||||
// qualityCmd represents the quality command
|
||||
var qualityCmd = &cobra.Command{
|
||||
Use: "quality",
|
||||
Short: "Code quality analysis and technical debt tracking",
|
||||
Long: `Analyze code quality issues like complexity, duplication, naming inconsistencies,
|
||||
and more. Supports multiple languages including Go, Python, TypeScript, Java, and Rust.
|
||||
|
||||
Examples:
|
||||
devour quality scan ./src # Scan current directory
|
||||
devour quality scan --lang go ./src # Scan with explicit language
|
||||
devour quality status # Show current status
|
||||
devour quality next # Show next priority issue
|
||||
devour quality resolve fixed 123 --note "Refactored" # Mark issue as fixed`,
|
||||
}
|
||||
|
||||
// scanCmd represents the scan subcommand
|
||||
var scanCmd = &cobra.Command{
|
||||
Use: "scan [path]",
|
||||
Short: "Run code quality analysis",
|
||||
Long: `Scan the given path for code quality issues. Automatically detects language
|
||||
unless specified with --lang flag.
|
||||
|
||||
The scan will detect:
|
||||
- Complexity issues (nested loops, excessive function calls)
|
||||
- Code duplication and near-duplicates
|
||||
- Naming inconsistencies across directories
|
||||
- Large files and god classes
|
||||
- Orphaned code and unused exports`,
|
||||
RunE: runQualityScan,
|
||||
}
|
||||
|
||||
// qualityStatusCmd represents the quality status subcommand
|
||||
var qualityStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show code quality status and scorecard",
|
||||
Long: `Display the current code quality status including:
|
||||
- Overall health score and grade
|
||||
- Findings broken down by type and severity
|
||||
- Progress metrics and next steps`,
|
||||
RunE: runQualityStatus,
|
||||
}
|
||||
|
||||
// nextCmd represents the next subcommand
|
||||
var nextCmd = &cobra.Command{
|
||||
Use: "next",
|
||||
Short: "Show the next highest priority issue to fix",
|
||||
Long: `Display the next highest priority finding based on severity and score.
|
||||
This helps you focus on the most impactful improvements first.`,
|
||||
RunE: runQualityNext,
|
||||
}
|
||||
|
||||
// resolveCmd represents the resolve subcommand
|
||||
var resolveCmd = &cobra.Command{
|
||||
Use: "resolve <status> <id>",
|
||||
Short: "Mark a finding as resolved",
|
||||
Long: `Mark a finding with a specific status:
|
||||
- fixed: Issue has been resolved
|
||||
- wontfix: Issue won't be fixed (valid reason required)
|
||||
- false_positive: Finding is incorrect
|
||||
- ignore: Temporarily ignore the finding
|
||||
|
||||
Examples:
|
||||
devour quality resolve fixed abc123 --note "Refactored complex function"
|
||||
devour quality resolve wontfix def456 --note "Legacy code, planned replacement"`,
|
||||
RunE: runQualityResolve,
|
||||
}
|
||||
|
||||
// Quality flags
|
||||
var (
|
||||
qualityPath string
|
||||
qualityLanguage string
|
||||
qualityExclude []string
|
||||
qualityThreshold int
|
||||
qualityMinLOC int
|
||||
qualityTargetScore int
|
||||
qualityFormat string
|
||||
qualityResetSubjective bool
|
||||
qualityNoBadge bool
|
||||
qualityBadgePath string
|
||||
)
|
||||
|
||||
var explain bool
|
||||
var tier int
|
||||
var resolveNote string
|
||||
var attest string
|
||||
var qualityUseAST bool
|
||||
var statusNarrative bool
|
||||
var fixDryRun bool
|
||||
var fixAll bool
|
||||
var reviewPrepare bool
|
||||
var reviewImport string
|
||||
|
||||
var fixCmd = &cobra.Command{
|
||||
Use: "fix [id]",
|
||||
Short: "Auto-fix code quality issues",
|
||||
Long: `Automatically fix T1 (auto-fixable) issues.
|
||||
|
||||
Examples:
|
||||
devour quality fix unused_import::file.go::fmt # Fix specific issue
|
||||
devour quality fix --all --dry-run # Preview all fixes
|
||||
devour quality fix --all # Fix all auto-fixable issues`,
|
||||
RunE: runQualityFix,
|
||||
}
|
||||
|
||||
var reviewCmd = &cobra.Command{
|
||||
Use: "review",
|
||||
Short: "Generate or import AI review packets",
|
||||
Long: `Generate a review packet for AI analysis, or import responses.
|
||||
|
||||
Examples:
|
||||
devour quality review --prepare # Generate review packet
|
||||
devour quality review --import responses.json # Import AI responses`,
|
||||
RunE: runQualityReview,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(qualityCmd)
|
||||
qualityCmd.AddCommand(scanCmd, qualityStatusCmd, nextCmd, resolveCmd, fixCmd, reviewCmd)
|
||||
|
||||
// Scan flags
|
||||
scanCmd.Flags().StringVar(&qualityPath, "path", ".", "Path to scan")
|
||||
scanCmd.Flags().StringVar(&qualityLanguage, "lang", "", "Language (auto-detected if not specified)")
|
||||
scanCmd.Flags().StringSliceVar(&qualityExclude, "exclude", []string{}, "Exclude patterns")
|
||||
scanCmd.Flags().IntVar(&qualityThreshold, "threshold", 15, "Minimum score to flag an issue")
|
||||
scanCmd.Flags().IntVar(&qualityMinLOC, "min-loc", 50, "Minimum lines of code to analyze")
|
||||
scanCmd.Flags().IntVar(&qualityTargetScore, "target-score", 95, "Target health score")
|
||||
scanCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json)")
|
||||
scanCmd.Flags().BoolVar(&qualityResetSubjective, "reset-subjective", false, "Reset subjective baseline")
|
||||
scanCmd.Flags().BoolVar(&qualityNoBadge, "no-badge", false, "Skip badge generation")
|
||||
scanCmd.Flags().StringVar(&qualityBadgePath, "badge-path", "scorecard.png", "Badge output path")
|
||||
|
||||
// Status flags
|
||||
qualityStatusCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json)")
|
||||
qualityStatusCmd.Flags().BoolVar(&statusNarrative, "narrative", false, "Include narrative analysis")
|
||||
|
||||
// Next flags
|
||||
nextCmd.Flags().BoolVar(&explain, "explain", false, "Show detailed explanation")
|
||||
nextCmd.Flags().IntVar(&tier, "tier", 0, "Filter by severity tier (1-4)")
|
||||
|
||||
// Resolve flags
|
||||
resolveCmd.Flags().StringVar(&resolveNote, "note", "", "Note explaining the resolution (required)")
|
||||
resolveCmd.Flags().StringVar(&attest, "attest", "", "Attestation of improvement")
|
||||
|
||||
// Fix flags
|
||||
fixCmd.Flags().BoolVar(&fixDryRun, "dry-run", false, "Show what would be fixed without making changes")
|
||||
fixCmd.Flags().BoolVar(&fixAll, "all", false, "Fix all auto-fixable issues")
|
||||
|
||||
// Review flags
|
||||
reviewCmd.Flags().BoolVar(&reviewPrepare, "prepare", false, "Generate review packet for AI analysis")
|
||||
reviewCmd.Flags().StringVar(&reviewImport, "import", "", "Import review responses from file")
|
||||
}
|
||||
|
||||
func runQualityScan(cmd *cobra.Command, args []string) error {
|
||||
path := qualityPath
|
||||
if len(args) > 0 {
|
||||
path = args[0]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("path does not exist: %s", path)
|
||||
}
|
||||
|
||||
config := &quality.Config{
|
||||
Path: path,
|
||||
Language: qualityLanguage,
|
||||
Exclude: qualityExclude,
|
||||
Threshold: qualityThreshold,
|
||||
MinLOC: qualityMinLOC,
|
||||
TargetScore: qualityTargetScore,
|
||||
ResetSubjective: qualityResetSubjective,
|
||||
NoBadge: qualityNoBadge,
|
||||
BadgePath: qualityBadgePath,
|
||||
}
|
||||
|
||||
scanner := quality.NewScanner(config)
|
||||
finder := quality.NewDefaultFileFinder()
|
||||
scanner.SetFileFinder(finder)
|
||||
|
||||
lang := qualityLanguage
|
||||
if lang == "" {
|
||||
lang = quality.DetectLanguage(path)
|
||||
fmt.Printf("Auto-detected language: %s\n", lang)
|
||||
}
|
||||
|
||||
plugin, ok := plugins.Get(lang)
|
||||
if ok {
|
||||
fmt.Printf("Using %s plugin with AST analysis\n", lang)
|
||||
for _, detector := range plugin.CreateDetectors(finder) {
|
||||
scanner.RegisterDetector(detector)
|
||||
}
|
||||
} else {
|
||||
scanner.RegisterDetector(detectors.NewComplexityDetector(finder))
|
||||
scanner.RegisterDetector(detectors.NewDuplicationDetector(finder))
|
||||
scanner.RegisterDetector(detectors.NewNamingDetector(finder))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := scanner.Scan(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan failed: %w", err)
|
||||
}
|
||||
|
||||
return outputScanResult(result, qualityFormat)
|
||||
}
|
||||
|
||||
func runQualityStatus(cmd *cobra.Command, args []string) error {
|
||||
// Load previous scan results
|
||||
dataDir := filepath.Join(".", "devour_data", "quality")
|
||||
statusFile := filepath.Join(dataDir, "status.json")
|
||||
|
||||
var findings []quality.Finding
|
||||
var lastScan time.Time
|
||||
|
||||
if data, err := os.ReadFile(statusFile); err == nil {
|
||||
var status struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &status); err == nil {
|
||||
findings = status.Findings
|
||||
lastScan = status.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
if len(findings) == 0 {
|
||||
fmt.Println("No previous scan results found. Run 'devour quality scan' first.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate scorecard
|
||||
scorer := quality.NewScorer(qualityTargetScore)
|
||||
scorecard := scorer.GenerateScorecard(findings, lastScan)
|
||||
|
||||
// Output based on format
|
||||
switch qualityFormat {
|
||||
case "json":
|
||||
return json.NewEncoder(os.Stdout).Encode(scorecard)
|
||||
default:
|
||||
fmt.Println(scorer.FormatScorecard(scorecard))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func runQualityNext(cmd *cobra.Command, args []string) error {
|
||||
// Load previous scan results
|
||||
dataDir := filepath.Join(".", "devour_data", "quality")
|
||||
statusFile := filepath.Join(dataDir, "status.json")
|
||||
|
||||
var findings []quality.Finding
|
||||
|
||||
if data, err := os.ReadFile(statusFile); err == nil {
|
||||
var status struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &status); err == nil {
|
||||
findings = status.Findings
|
||||
}
|
||||
}
|
||||
|
||||
if len(findings) == 0 {
|
||||
fmt.Println("No findings found. Run 'devour quality scan' first.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get next priority finding
|
||||
scorer := quality.NewScorer(qualityTargetScore)
|
||||
next := scorer.GetNextPriority(findings)
|
||||
|
||||
if next == nil {
|
||||
fmt.Println("🎉 No open issues to fix!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter by tier if specified
|
||||
if tier > 0 {
|
||||
if int(next.Severity) != tier {
|
||||
// Find next in specified tier
|
||||
for _, finding := range findings {
|
||||
if finding.Status == quality.StatusOpen && int(finding.Severity) == tier {
|
||||
next = &finding
|
||||
break
|
||||
}
|
||||
}
|
||||
if next == nil || int(next.Severity) != tier {
|
||||
fmt.Printf("No open issues found in tier %d.\n", tier)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display finding
|
||||
fmt.Printf("Next Priority Issue (T%d)\n", int(next.Severity))
|
||||
fmt.Println("=======================================")
|
||||
fmt.Printf("File: %s:%d\n", next.File, next.Line)
|
||||
fmt.Printf("Title: %s\n", next.Title)
|
||||
fmt.Printf("Score: %d\n", next.Score)
|
||||
fmt.Printf("ID: %s\n", next.ID)
|
||||
fmt.Printf("\nDescription:\n%s\n", next.Description)
|
||||
|
||||
if explain {
|
||||
fmt.Printf("\nExplanation:\n")
|
||||
fmt.Printf("This is a T%d severity issue. ", int(next.Severity))
|
||||
switch next.Severity {
|
||||
case quality.SeverityT1:
|
||||
fmt.Println("T1 issues are typically auto-fixable like unused imports or debug logs.")
|
||||
case quality.SeverityT2:
|
||||
fmt.Println("T2 issues require quick manual fixes like unused variables or dead exports.")
|
||||
case quality.SeverityT3:
|
||||
fmt.Println("T3 issues need judgment calls like near-duplicates or single-use abstractions.")
|
||||
case quality.SeverityT4:
|
||||
fmt.Println("T4 issues require major refactoring like god components or mixed concerns.")
|
||||
}
|
||||
|
||||
fmt.Printf("\nTo fix: devour quality resolve fixed %s --note \"Describe what you fixed\"\n", next.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runQualityResolve(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("usage: devour quality resolve <status> <id>")
|
||||
}
|
||||
|
||||
status := quality.Status(args[0])
|
||||
id := args[1]
|
||||
|
||||
// Validate status
|
||||
validStatuses := map[quality.Status]bool{
|
||||
quality.StatusFixed: true,
|
||||
quality.StatusWontfix: true,
|
||||
quality.StatusFalsePositive: true,
|
||||
quality.StatusIgnored: true,
|
||||
}
|
||||
|
||||
if !validStatuses[status] {
|
||||
return fmt.Errorf("invalid status: %s", status)
|
||||
}
|
||||
|
||||
if resolveNote == "" {
|
||||
return fmt.Errorf("--note is required when resolving findings")
|
||||
}
|
||||
|
||||
// Load current findings
|
||||
dataDir := filepath.Join(".", "devour_data", "quality")
|
||||
statusFile := filepath.Join(dataDir, "status.json")
|
||||
|
||||
var findings []quality.Finding
|
||||
|
||||
if data, err := os.ReadFile(statusFile); err == nil {
|
||||
var statusData struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &statusData); err == nil {
|
||||
findings = statusData.Findings
|
||||
}
|
||||
}
|
||||
|
||||
// Find and update the finding
|
||||
found := false
|
||||
for i, finding := range findings {
|
||||
if finding.ID == id {
|
||||
findings[i].Status = status
|
||||
findings[i].UpdatedAt = time.Now()
|
||||
if finding.Metadata == nil {
|
||||
findings[i].Metadata = make(map[string]string)
|
||||
}
|
||||
findings[i].Metadata["resolution_note"] = resolveNote
|
||||
if attest != "" {
|
||||
findings[i].Metadata["attestation"] = attest
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("finding not found: %s", id)
|
||||
}
|
||||
|
||||
// Save updated findings
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
statusData := struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}{
|
||||
Findings: findings,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(statusData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal status: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(statusFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to save status: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Resolved: %s as %s\n", id, status)
|
||||
if resolveNote != "" {
|
||||
fmt.Printf("Note: %s\n", resolveNote)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputScanResult(result *quality.ScanResult, format string) error {
|
||||
// Save results to data directory
|
||||
dataDir := filepath.Join(".", "devour_data", "quality")
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
statusFile := filepath.Join(dataDir, "status.json")
|
||||
statusData := struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}{
|
||||
Findings: result.Findings,
|
||||
Timestamp: result.Timestamp,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(statusData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal results: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(statusFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to save results: %w", err)
|
||||
}
|
||||
|
||||
// Generate scorecard badge if not disabled
|
||||
if !qualityNoBadge && qualityBadgePath != "" {
|
||||
if err := generateScorecardBadge(result, qualityBadgePath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to generate scorecard badge: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Scorecard badge generated: %s\n", qualityBadgePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Output based on format
|
||||
switch format {
|
||||
case "json":
|
||||
return json.NewEncoder(os.Stdout).Encode(result)
|
||||
default:
|
||||
return formatScanResultText(result)
|
||||
}
|
||||
}
|
||||
|
||||
func generateScorecardBadge(result *quality.ScanResult, outputPath string) error {
|
||||
// Calculate grade from score
|
||||
scorer := quality.NewScorer(qualityTargetScore)
|
||||
grade := scorer.GetHealthGrade(result.StrictScore)
|
||||
|
||||
// Group findings by type and tier
|
||||
findByType := make(map[string]int)
|
||||
findByTier := make(map[string]int)
|
||||
for _, f := range result.Findings {
|
||||
findByType[f.Type]++
|
||||
tierName := fmt.Sprintf("T%d", int(f.Severity))
|
||||
findByTier[tierName]++
|
||||
}
|
||||
|
||||
// Get project name from current directory
|
||||
projectName := "devour"
|
||||
if dir, err := os.Getwd(); err == nil {
|
||||
projectName = filepath.Base(dir)
|
||||
}
|
||||
|
||||
// Prepare scorecard data
|
||||
scoreData := &scorecard.ScorecardData{
|
||||
ProjectName: projectName,
|
||||
Version: "", // Could be extracted from version info
|
||||
OverallScore: float64(result.Score),
|
||||
StrictScore: float64(result.StrictScore),
|
||||
Grade: grade,
|
||||
FindingsTotal: len(result.Findings),
|
||||
FindingsOpen: len(result.Findings), // All findings are open initially
|
||||
LastScan: result.Timestamp,
|
||||
FindByType: findByType,
|
||||
FindByTier: findByTier,
|
||||
}
|
||||
|
||||
return scorecard.Generate(scoreData, outputPath)
|
||||
}
|
||||
|
||||
func formatScanResultText(result *quality.ScanResult) error {
|
||||
fmt.Println("Code Quality Scan Results")
|
||||
fmt.Println("=======================================")
|
||||
fmt.Printf("Files checked: %d\n", result.FilesChecked)
|
||||
fmt.Printf("Duration: %s\n", result.Duration)
|
||||
fmt.Printf("Score: %d (strict: %d)\n", result.Score, result.StrictScore)
|
||||
fmt.Printf("Findings: %d\n\n", len(result.Findings))
|
||||
|
||||
if len(result.Findings) == 0 {
|
||||
fmt.Println("No code quality issues found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
bySeverity := make(map[quality.Severity][]quality.Finding)
|
||||
for _, finding := range result.Findings {
|
||||
bySeverity[finding.Severity] = append(bySeverity[finding.Severity], finding)
|
||||
}
|
||||
|
||||
tiers := []quality.Severity{quality.SeverityT4, quality.SeverityT3, quality.SeverityT2, quality.SeverityT1}
|
||||
tierNames := map[quality.Severity]string{
|
||||
quality.SeverityT1: "T1 (Auto-fixable)",
|
||||
quality.SeverityT2: "T2 (Quick manual)",
|
||||
quality.SeverityT3: "T3 (Needs judgment)",
|
||||
quality.SeverityT4: "T4 (Major refactor)",
|
||||
}
|
||||
|
||||
for _, severity := range tiers {
|
||||
findings := bySeverity[severity]
|
||||
if len(findings) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] %d issues\n", tierNames[severity], len(findings))
|
||||
for _, finding := range findings {
|
||||
fmt.Printf(" - %s:%d - %s (score: %d)\n",
|
||||
filepath.Base(finding.File), finding.Line, finding.Title, finding.Score)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("Run 'devour quality next' to see the highest priority issue to fix.")
|
||||
fmt.Println("Run 'devour quality status' for detailed scorecard.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runQualityFix(cmd *cobra.Command, args []string) error {
|
||||
dataDir := filepath.Join(".", "devour_data", "quality")
|
||||
statusFile := filepath.Join(dataDir, "status.json")
|
||||
|
||||
var findings []quality.Finding
|
||||
if data, err := os.ReadFile(statusFile); err == nil {
|
||||
var status struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &status); err == nil {
|
||||
findings = status.Findings
|
||||
}
|
||||
}
|
||||
|
||||
if len(findings) == 0 {
|
||||
fmt.Println("No findings found. Run 'devour quality scan' first.")
|
||||
return nil
|
||||
}
|
||||
|
||||
availableFixers := []plugins.Fixer{
|
||||
fixers.NewUnusedImportFixer(),
|
||||
fixers.NewFormattingFixer(),
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
fixed := 0
|
||||
var errors []string
|
||||
|
||||
if fixAll {
|
||||
for _, finding := range findings {
|
||||
if finding.Status != quality.StatusOpen || finding.Severity != quality.SeverityT1 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fixer := range availableFixers {
|
||||
if fixer.CanFix(finding) {
|
||||
result, err := fixer.Fix(ctx, finding, fixDryRun)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %v", finding.ID, err))
|
||||
continue
|
||||
}
|
||||
if result.Success {
|
||||
fixed++
|
||||
fmt.Printf("[OK] %s\n", result.Message)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("specify a finding ID or use --all")
|
||||
}
|
||||
|
||||
targetID := args[0]
|
||||
var target *quality.Finding
|
||||
for i := range findings {
|
||||
if findings[i].ID == targetID {
|
||||
target = &findings[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
return fmt.Errorf("finding not found: %s", targetID)
|
||||
}
|
||||
|
||||
for _, fixer := range availableFixers {
|
||||
if fixer.CanFix(*target) {
|
||||
result, err := fixer.Fix(ctx, *target, fixDryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fix failed: %w", err)
|
||||
}
|
||||
fmt.Printf("[OK] %s\n", result.Message)
|
||||
fixed = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fixDryRun {
|
||||
fmt.Printf("\nDry run complete. %d issues would be fixed.\n", fixed)
|
||||
} else {
|
||||
fmt.Printf("\nFixed %d issues.\n", fixed)
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
fmt.Printf("\nErrors:\n")
|
||||
for _, e := range errors {
|
||||
fmt.Printf(" • %s\n", e)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runQualityReview(cmd *cobra.Command, args []string) error {
|
||||
dataDir := filepath.Join(".", "devour_data")
|
||||
|
||||
if reviewPrepare {
|
||||
return prepareReviewPacket(dataDir)
|
||||
}
|
||||
|
||||
if reviewImport != "" {
|
||||
return importReviewResponses(dataDir, reviewImport)
|
||||
}
|
||||
|
||||
return fmt.Errorf("use --prepare to generate a review packet or --import <file> to import responses")
|
||||
}
|
||||
|
||||
func prepareReviewPacket(dataDir string) error {
|
||||
statusFile := filepath.Join(dataDir, "quality", "status.json")
|
||||
|
||||
var findings []quality.Finding
|
||||
var lastScan time.Time
|
||||
|
||||
if data, err := os.ReadFile(statusFile); err == nil {
|
||||
var status struct {
|
||||
Findings []quality.Finding `json:"findings"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &status); err == nil {
|
||||
findings = status.Findings
|
||||
lastScan = status.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
scorer := quality.NewScorer(qualityTargetScore)
|
||||
scorecard := scorer.GenerateScorecard(findings, lastScan)
|
||||
|
||||
gen := review.NewPacketGenerator(dataDir)
|
||||
packet, err := gen.Generate(findings, scorecard, "go")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate review packet: %w", err)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("review-%s.json", time.Now().Format("20060102-150405"))
|
||||
if err := gen.Save(packet, filename); err != nil {
|
||||
return fmt.Errorf("failed to save review packet: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Review packet generated: %s/review/%s\n", dataDir, filename)
|
||||
fmt.Printf("Findings to review: %d\n", len(packet.Findings))
|
||||
fmt.Printf("Questions: %d\n", len(packet.Questions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importReviewResponses(dataDir string, filename string) error {
|
||||
gen := review.NewPacketGenerator(dataDir)
|
||||
|
||||
responses := make(map[string]string)
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read responses file: %w", err)
|
||||
}
|
||||
|
||||
var respData struct {
|
||||
Responses map[string]string `json:"responses"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &respData); err == nil {
|
||||
responses = respData.Responses
|
||||
} else {
|
||||
var simpleResponses map[string]string
|
||||
if err := json.Unmarshal(data, &simpleResponses); err != nil {
|
||||
return fmt.Errorf("failed to parse responses: %w", err)
|
||||
}
|
||||
responses = simpleResponses
|
||||
}
|
||||
|
||||
if err := gen.ImportReview(filepath.Base(filename), responses); err != nil {
|
||||
return fmt.Errorf("failed to import responses: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Imported %d review responses\n", len(responses))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var queryCmd = &cobra.Command{
|
||||
Use: "query <text>",
|
||||
Short: "Search indexed documentation",
|
||||
Long: `Search indexed documentation using semantic similarity.
|
||||
|
||||
Returns the most relevant document chunks based on vector similarity
|
||||
to the query text.
|
||||
|
||||
Examples:
|
||||
devour query "how to authenticate"
|
||||
devour query "API rate limiting" --limit 10
|
||||
devour query "deployment" --format json`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: runQuery,
|
||||
}
|
||||
|
||||
var (
|
||||
queryLimit int
|
||||
queryFormat string
|
||||
queryThreshold float64
|
||||
)
|
||||
|
||||
func init() {
|
||||
queryCmd.Flags().IntVarP(&queryLimit, "limit", "l", 5, "maximum number of results")
|
||||
queryCmd.Flags().StringVarP(&queryFormat, "format", "f", "text", "output format (text, json, markdown)")
|
||||
queryCmd.Flags().Float64Var(&queryThreshold, "threshold", 0.7, "similarity threshold (0-1)")
|
||||
}
|
||||
|
||||
func runQuery(cmd *cobra.Command, args []string) error {
|
||||
query := args[0]
|
||||
if len(args) > 1 {
|
||||
query = fmt.Sprintf("%s", args)
|
||||
}
|
||||
|
||||
fmt.Printf("Searching: %q\n", query)
|
||||
fmt.Printf(" Limit: %d\n", queryLimit)
|
||||
fmt.Printf(" Threshold: %.2f\n", queryThreshold)
|
||||
fmt.Println()
|
||||
|
||||
// TODO: Implement actual query logic
|
||||
// 1. Generate embedding for query
|
||||
// 2. Search vector database
|
||||
// 3. Format and return results
|
||||
|
||||
// Placeholder results
|
||||
fmt.Println("Results:")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
fmt.Println("⚠️ Query functionality not yet implemented")
|
||||
fmt.Println(" Index some documents first with 'devour scrape'")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/yourorg/devour/internal/scraper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== Devour Real HTTP Scraping Test ===")
|
||||
fmt.Println()
|
||||
|
||||
config := &scraper.Config{
|
||||
UserAgent: "Devour/1.0 (Documentation Scraper)",
|
||||
Timeout: 30 * time.Second,
|
||||
RetryCount: 3,
|
||||
RetryDelay: 1 * time.Second,
|
||||
Concurrency: 10,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sources := []struct {
|
||||
name string
|
||||
st scraper.SourceType
|
||||
url string
|
||||
}{
|
||||
{"Go stdlib net/http", scraper.SourceTypeGoDocs, "https://pkg.go.dev/net/http"},
|
||||
{"Spring AI MCP", scraper.SourceTypeSpringDocs, "https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html"},
|
||||
{"React Hooks", scraper.SourceTypeReactDocs, "https://react.dev/reference/react"},
|
||||
{"Vue Composition API", scraper.SourceTypeVueDocs, "https://vuejs.org/api/"},
|
||||
{"Cloudflare Docs", scraper.SourceTypeCloudflareDocs, "https://developers.cloudflare.com/"},
|
||||
}
|
||||
|
||||
for _, src := range sources {
|
||||
fmt.Printf("=== Testing: %s ===\n", src.name)
|
||||
|
||||
s := scraper.NewScraper(src.st, config)
|
||||
if s == nil {
|
||||
fmt.Printf(" ✗ Scraper not available for type: %s\n\n", src.st)
|
||||
continue
|
||||
}
|
||||
|
||||
source := &scraper.Source{
|
||||
Name: src.name,
|
||||
Type: src.st,
|
||||
URL: src.url,
|
||||
}
|
||||
|
||||
fmt.Printf(" Fetching: %s\n", src.url)
|
||||
|
||||
docs, err := s.Scrape(ctx, source)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Error: %v\n\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Scraped %d documents\n", len(docs))
|
||||
|
||||
if len(docs) > 0 {
|
||||
first := docs[0]
|
||||
fmt.Printf(" First document:\n")
|
||||
fmt.Printf(" Title: %s\n", first.Title)
|
||||
fmt.Printf(" Type: %s\n", first.Type)
|
||||
if len(first.Content) > 100 {
|
||||
fmt.Printf(" Content preview: %s...\n", first.Content[:100])
|
||||
} else {
|
||||
fmt.Printf(" Content: %s\n", first.Content)
|
||||
}
|
||||
}
|
||||
|
||||
changed, hash, err := s.DetectChanges(ctx, source, "")
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Change detection error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Change detection: changed=%v, hash=%s\n", changed, hash[:16]+"...")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("=== All Source Types ===")
|
||||
fmt.Println()
|
||||
fmt.Println("Available scrapers:")
|
||||
allTypes := []scraper.SourceType{
|
||||
scraper.SourceTypeWeb,
|
||||
scraper.SourceTypeGitHub,
|
||||
scraper.SourceTypeOpenAPI,
|
||||
scraper.SourceTypeLocal,
|
||||
scraper.SourceTypeGoDocs,
|
||||
scraper.SourceTypeRustDocs,
|
||||
scraper.SourceTypePythonDocs,
|
||||
scraper.SourceTypeJavaDocs,
|
||||
scraper.SourceTypeSpringDocs,
|
||||
scraper.SourceTypeTSDocs,
|
||||
scraper.SourceTypeReactDocs,
|
||||
scraper.SourceTypeVueDocs,
|
||||
scraper.SourceTypeNuxtDocs,
|
||||
scraper.SourceTypeMCPDocs,
|
||||
scraper.SourceTypeDockerDocs,
|
||||
scraper.SourceTypeCloudflareDocs,
|
||||
scraper.SourceTypeAstroDocs,
|
||||
}
|
||||
|
||||
for _, st := range allTypes {
|
||||
s := scraper.NewScraper(st, config)
|
||||
if s != nil {
|
||||
fmt.Printf(" ✓ %s\n", st)
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s (not implemented)\n", st)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== Test Complete ===")
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/yourorg/devour/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
verbose bool
|
||||
showLogo bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "devour",
|
||||
Short: "Context ingestion and management for AI",
|
||||
Long: ` ___
|
||||
.-' '-.
|
||||
/ O O \
|
||||
| _ |
|
||||
\ '-' /
|
||||
'-.___.-'
|
||||
Documentation Devourer
|
||||
|
||||
Devour scrapes, indexes, and serves documentation from multiple sources
|
||||
(GitHub repos, OpenAPI specs, web docs, local files) to feed structured
|
||||
context to AI models for generating accurate, fully working code.
|
||||
|
||||
Runs in two modes:
|
||||
- Local mode: OpenCode skill running entirely on your machine
|
||||
- Remote mode: MCP server for multi-user/team access`,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is ./devour.yaml)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose logging")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showLogo, "logo", "l", false, "show the Devour character logo")
|
||||
|
||||
rootCmd.AddCommand(initCmd)
|
||||
rootCmd.AddCommand(scrapeCmd)
|
||||
rootCmd.AddCommand(getCmd)
|
||||
rootCmd.AddCommand(languagesCmd)
|
||||
rootCmd.AddCommand(demoCmd)
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
rootCmd.AddCommand(queryCmd)
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
rootCmd.AddCommand(syncCmd)
|
||||
rootCmd.AddCommand(pushCmd)
|
||||
rootCmd.AddCommand(logoCmd)
|
||||
}
|
||||
|
||||
// logoCmd displays the Devour character
|
||||
var logoCmd = &cobra.Command{
|
||||
Use: "logo",
|
||||
Short: "Display the Devour character logo",
|
||||
Long: `Display the ASCII art Devour character mascot.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ui.PrintCharacter()
|
||||
},
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("$HOME/.devour")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName("devour")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("DEVOUR")
|
||||
|
||||
if err := viper.ReadInConfig(); err == nil && verbose {
|
||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yourorg/devour/internal/markdown"
|
||||
"github.com/yourorg/devour/internal/scraper"
|
||||
)
|
||||
|
||||
var scrapeCmd = &cobra.Command{
|
||||
Use: "scrape <source>",
|
||||
Short: "Scrape documentation from a source",
|
||||
Long: `Scrape and index documentation from various sources.
|
||||
|
||||
Supported source types:
|
||||
- godocs: Go packages (pkg.go.dev)
|
||||
- rustdocs: Rust crates (docs.rs)
|
||||
- pythondocs: Python modules (docs.python.org)
|
||||
- javadocs: Java packages (docs.oracle.com)
|
||||
- springdocs: Spring Boot (docs.spring.io)
|
||||
- tsdocs: TypeScript (typescriptlang.org)
|
||||
- reactdocs: React (react.dev)
|
||||
- vuedocs: Vue.js (vuejs.org)
|
||||
- nuxtdocs: Nuxt (nuxt.com)
|
||||
- mcpdocs: MCP servers (hub.docker.com/mcp)
|
||||
- dockerdocs: Docker (docs.docker.com)
|
||||
- cloudflaredocs: Cloudflare (developers.cloudflare.com)
|
||||
- astrodocs: Astro (docs.astro.build)
|
||||
- url: Generic web pages
|
||||
- github: GitHub repositories
|
||||
|
||||
Examples:
|
||||
devour scrape https://pkg.go.dev/net/http --type godocs
|
||||
devour scrape https://react.dev/reference/react --type reactdocs
|
||||
devour scrape https://developers.cloudflare.com/ --type cloudflaredocs
|
||||
devour scrape --sources sources.yaml`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runScrape,
|
||||
}
|
||||
|
||||
var (
|
||||
scrapeFormat string
|
||||
scrapeSources string
|
||||
scrapeOutput string
|
||||
scrapeConcurrency int
|
||||
scrapeType string
|
||||
)
|
||||
|
||||
func init() {
|
||||
scrapeCmd.Flags().StringVarP(&scrapeFormat, "format", "f", "json", "output format (json, markdown)")
|
||||
scrapeCmd.Flags().StringVarP(&scrapeSources, "sources", "s", "", "YAML file with source definitions")
|
||||
scrapeCmd.Flags().StringVarP(&scrapeOutput, "output", "o", "", "output directory (default: devour_data/docs)")
|
||||
scrapeCmd.Flags().IntVar(&scrapeConcurrency, "concurrency", 10, "parallel scraping workers")
|
||||
scrapeCmd.Flags().StringVarP(&scrapeType, "type", "t", "", "source type (auto-detected if not specified)")
|
||||
}
|
||||
|
||||
func runScrape(cmd *cobra.Command, args []string) error {
|
||||
if scrapeSources != "" {
|
||||
return scrapeFromConfig(scrapeSources)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("source argument required when not using --sources flag")
|
||||
}
|
||||
|
||||
sourceURL := args[0]
|
||||
|
||||
config := &scraper.Config{
|
||||
UserAgent: "Devour/1.0 (Documentation Scraper)",
|
||||
Timeout: 30 * time.Second,
|
||||
RetryCount: 3,
|
||||
RetryDelay: 1 * time.Second,
|
||||
Concurrency: scrapeConcurrency,
|
||||
}
|
||||
|
||||
sourceType := scraper.SourceType(scrapeType)
|
||||
if sourceType == "" {
|
||||
sourceType = detectSourceType(sourceURL)
|
||||
}
|
||||
|
||||
fmt.Printf("Scraping: %s\n", sourceURL)
|
||||
fmt.Printf(" Type: %s\n", sourceType)
|
||||
fmt.Printf(" Concurrency: %d\n", scrapeConcurrency)
|
||||
fmt.Println()
|
||||
|
||||
s := scraper.NewScraper(sourceType, config)
|
||||
if s == nil {
|
||||
return fmt.Errorf("unsupported source type: %s", sourceType)
|
||||
}
|
||||
|
||||
source := &scraper.Source{
|
||||
Name: extractName(sourceURL),
|
||||
Type: sourceType,
|
||||
URL: sourceURL,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
docs, err := s.Scrape(ctx, source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scraping failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Scraped %d documents\n\n", len(docs))
|
||||
|
||||
if scrapeOutput == "" {
|
||||
scrapeOutput = "devour_data/docs"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(scrapeOutput, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
for i, doc := range docs {
|
||||
var filename string
|
||||
var content []byte
|
||||
|
||||
if scrapeFormat == "markdown" {
|
||||
filename = fmt.Sprintf("%s_%d.md", sanitizeFilename(doc.Title), i)
|
||||
|
||||
// Create enhanced markdown document
|
||||
markdownDoc := &markdown.Document{
|
||||
ID: doc.ID,
|
||||
Source: doc.Source,
|
||||
Type: string(doc.Type),
|
||||
Title: doc.Title,
|
||||
Content: doc.Content,
|
||||
URL: doc.URL,
|
||||
Metadata: doc.Metadata,
|
||||
Hash: doc.Hash,
|
||||
Timestamp: doc.Timestamp,
|
||||
}
|
||||
|
||||
formatter := markdown.NewFormatter()
|
||||
content = []byte(formatter.FormatWithTOC(markdownDoc))
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s_%d.json", sanitizeFilename(doc.Title), i)
|
||||
content, err = json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal document: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
filePath := filepath.Join(scrapeOutput, filename)
|
||||
if err := os.WriteFile(filePath, content, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write document: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" 📄 %s (%s)\n", filename, doc.Type)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Scraping complete!\n")
|
||||
fmt.Printf(" Output: %s\n", scrapeOutput)
|
||||
fmt.Println(" Run 'devour status' to see indexed documents")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scrapeFromConfig(configPath string) error {
|
||||
return fmt.Errorf("scraping from config file not yet implemented")
|
||||
}
|
||||
|
||||
func detectSourceType(sourceURL string) scraper.SourceType {
|
||||
u, err := url.Parse(sourceURL)
|
||||
if err != nil {
|
||||
return scraper.SourceTypeWeb
|
||||
}
|
||||
|
||||
host := u.Host
|
||||
path := u.Path
|
||||
|
||||
switch {
|
||||
case host == "pkg.go.dev" || strings.HasSuffix(host, "pkg.go.dev"):
|
||||
return scraper.SourceTypeGoDocs
|
||||
case host == "docs.rs" || host == "doc.rust-lang.org":
|
||||
return scraper.SourceTypeRustDocs
|
||||
case host == "docs.python.org":
|
||||
return scraper.SourceTypePythonDocs
|
||||
case host == "docs.oracle.com":
|
||||
return scraper.SourceTypeJavaDocs
|
||||
case host == "docs.spring.io":
|
||||
return scraper.SourceTypeSpringDocs
|
||||
case host == "www.typescriptlang.org" || host == "typescriptlang.org":
|
||||
return scraper.SourceTypeTSDocs
|
||||
case host == "react.dev":
|
||||
return scraper.SourceTypeReactDocs
|
||||
case host == "vuejs.org":
|
||||
return scraper.SourceTypeVueDocs
|
||||
case host == "nuxt.com":
|
||||
return scraper.SourceTypeNuxtDocs
|
||||
case strings.Contains(host, "docker.com") || host == "docs.docker.com":
|
||||
if strings.Contains(path, "/mcp/") {
|
||||
return scraper.SourceTypeMCPDocs
|
||||
}
|
||||
return scraper.SourceTypeDockerDocs
|
||||
case host == "developers.cloudflare.com":
|
||||
return scraper.SourceTypeCloudflareDocs
|
||||
case host == "docs.astro.build":
|
||||
return scraper.SourceTypeAstroDocs
|
||||
case host == "github.com":
|
||||
return scraper.SourceTypeGitHub
|
||||
default:
|
||||
return scraper.SourceTypeWeb
|
||||
}
|
||||
}
|
||||
|
||||
func extractName(sourceURL string) string {
|
||||
u, err := url.Parse(sourceURL)
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
return u.Host
|
||||
}
|
||||
|
||||
func sanitizeFilename(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
name = strings.ReplaceAll(name, "/", "_")
|
||||
name = strings.ReplaceAll(name, ":", "_")
|
||||
name = strings.ReplaceAll(name, ".", "_")
|
||||
|
||||
if len(name) > 50 {
|
||||
name = name[:50]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the MCP server",
|
||||
Long: `Start the Devour MCP server.
|
||||
|
||||
In local mode (default), the server communicates via stdio, making it
|
||||
suitable for use as an OpenCode skill.
|
||||
|
||||
In remote mode (--remote flag), the server listens on HTTP and exposes
|
||||
a REST API for multi-user access.
|
||||
|
||||
Examples:
|
||||
devour serve # Local mode (stdio)
|
||||
devour serve --remote # Remote mode on default port
|
||||
devour serve --remote --port 3000`,
|
||||
RunE: runServe,
|
||||
}
|
||||
|
||||
var (
|
||||
serveRemote bool
|
||||
servePort int
|
||||
serveHost string
|
||||
)
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().BoolVar(&serveRemote, "remote", false, "run as remote HTTP server")
|
||||
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080, "HTTP port (remote mode only)")
|
||||
serveCmd.Flags().StringVar(&serveHost, "host", "localhost", "HTTP host (remote mode only)")
|
||||
}
|
||||
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
if serveRemote {
|
||||
fmt.Printf("🚀 Starting Devour server in remote mode\n")
|
||||
fmt.Printf(" Host: %s\n", serveHost)
|
||||
fmt.Printf(" Port: %d\n", servePort)
|
||||
fmt.Printf(" URL: http://%s:%d\n", serveHost, servePort)
|
||||
|
||||
// TODO: Start HTTP MCP server
|
||||
return fmt.Errorf("remote mode not yet implemented")
|
||||
}
|
||||
|
||||
fmt.Println("🚀 Starting Devour server in local mode (stdio)")
|
||||
fmt.Println(" Communicating via JSON-RPC over stdin/stdout")
|
||||
|
||||
// TODO: Start stdio MCP server
|
||||
// Should handle JSON-RPC messages for:
|
||||
// - devour_query
|
||||
// - devour_add
|
||||
// - devour_status
|
||||
// - devour_sync
|
||||
|
||||
return fmt.Errorf("local mode not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yourorg/devour/internal/ui"
|
||||
)
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show index status and statistics",
|
||||
Long: `Display the current status of the Devour index.
|
||||
|
||||
Shows:
|
||||
- Index health
|
||||
- Document count
|
||||
- Last update time
|
||||
- Source status
|
||||
- Storage usage`,
|
||||
RunE: runStatus,
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
// Print the small character mascot
|
||||
ui.PrintCharacterSmall()
|
||||
fmt.Println()
|
||||
|
||||
ui.PrintHeader("Devour Status")
|
||||
|
||||
// TODO: Implement actual status check
|
||||
// Check:
|
||||
// - Index existence and health
|
||||
// - Document count
|
||||
// - Vector count
|
||||
// - Last sync time
|
||||
// - Source status
|
||||
|
||||
// Placeholder status
|
||||
ui.PrintKeyValue("Index Health", "⚠️ Not initialized")
|
||||
ui.PrintKeyValue("Documents", "0 indexed")
|
||||
ui.PrintKeyValue("Chunks", "0 total")
|
||||
ui.PrintKeyValue("Vector Dimension", "1536")
|
||||
ui.PrintKeyValue("Last Updated", "Never")
|
||||
ui.PrintKeyValue("Storage Used", "0 MB")
|
||||
|
||||
fmt.Println()
|
||||
ui.PrintSection("Sources")
|
||||
ui.PrintInfo(" None configured")
|
||||
|
||||
fmt.Println()
|
||||
ui.PrintSection("Next Steps")
|
||||
fmt.Println(" 1. Run 'devour init' to initialize")
|
||||
fmt.Println(" 2. Run 'devour scrape <source>' to index documents")
|
||||
|
||||
// Show when check happened
|
||||
fmt.Printf("\nStatus as of: %s\n", time.Now().Format(time.RFC3339))
|
||||
|
||||
return nil
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var syncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Synchronize with configured sources",
|
||||
Long: `Fetch updates from all configured sources.
|
||||
|
||||
Checks each source for changes (using hash or timestamp comparison)
|
||||
and updates the index accordingly.
|
||||
|
||||
Examples:
|
||||
devour sync # Sync all sources
|
||||
devour sync --source my-docs # Sync specific source
|
||||
devour sync --rebuild # Full rebuild`,
|
||||
RunE: runSync,
|
||||
}
|
||||
|
||||
var (
|
||||
syncSource string
|
||||
syncRebuild bool
|
||||
syncForce bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
syncCmd.Flags().StringVarP(&syncSource, "source", "s", "", "sync specific source only")
|
||||
syncCmd.Flags().BoolVar(&syncRebuild, "rebuild", false, "rebuild entire index")
|
||||
syncCmd.Flags().BoolVarP(&syncForce, "force", "f", false, "force sync even if no changes detected")
|
||||
}
|
||||
|
||||
func runSync(cmd *cobra.Command, args []string) error {
|
||||
if syncRebuild {
|
||||
fmt.Println("🔄 Rebuilding index from all sources...")
|
||||
} else {
|
||||
fmt.Println("🔄 Syncing with configured sources...")
|
||||
}
|
||||
|
||||
if syncSource != "" {
|
||||
fmt.Printf(" Source: %s\n", syncSource)
|
||||
}
|
||||
|
||||
// TODO: Implement actual sync logic
|
||||
// 1. Load sources from config
|
||||
// 2. For each source:
|
||||
// a. Check for changes (hash/timestamp)
|
||||
// b. If changes detected or --force:
|
||||
// - Scrape updated content
|
||||
// - Re-generate embeddings
|
||||
// - Update index
|
||||
// 3. Update metadata
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("⚠️ Sync functionality not yet implemented")
|
||||
fmt.Println(" Configure sources in devour.yaml first")
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user