@weibaohui/dsh-git-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +43 -0
  2. package/client/bundle.js +2356 -0
  3. package/client/index.js +2342 -0
  4. package/cordis.patch.yml +11 -0
  5. package/package.json +70 -0
  6. package/server/dist/api/v1.js +2029 -0
  7. package/server/dist/authx/password.js +43 -0
  8. package/server/dist/authx/um.js +157 -0
  9. package/server/dist/conf.js +323 -0
  10. package/server/dist/context.js +701 -0
  11. package/server/dist/db/actions.js +202 -0
  12. package/server/dist/db/db.js +838 -0
  13. package/server/dist/dshapi.js +1416 -0
  14. package/server/dist/editorconfig.js +104 -0
  15. package/server/dist/gitx/git.js +580 -0
  16. package/server/dist/gitx/http.js +232 -0
  17. package/server/dist/gitx/paths.js +28 -0
  18. package/server/dist/gitx/service.js +251 -0
  19. package/server/dist/gosprintf.js +167 -0
  20. package/server/dist/hook.js +53 -0
  21. package/server/dist/index.js +112 -0
  22. package/server/dist/lfsx.js +265 -0
  23. package/server/dist/mailer.js +127 -0
  24. package/server/dist/markup.js +317 -0
  25. package/server/dist/metrics.js +18 -0
  26. package/server/dist/mirror.js +203 -0
  27. package/server/dist/multipart.js +33 -0
  28. package/server/dist/repox.js +99 -0
  29. package/server/dist/router.js +185 -0
  30. package/server/dist/serv.js +43 -0
  31. package/server/dist/server.js +382 -0
  32. package/server/dist/sshx/dispatch.js +160 -0
  33. package/server/dist/sshx/lfs-transfer.js +289 -0
  34. package/server/dist/sshx/server.js +107 -0
  35. package/server/dist/toolx.js +108 -0
  36. package/server/dist/twofactor.js +160 -0
  37. package/server/dist/webapi.js +377 -0
  38. package/server/dist/webhook.js +128 -0
  39. package/server/package.json +34 -0
  40. package/server/public/img/avatar_default.png +0 -0
  41. package/server/tsconfig.json +19 -0
  42. package/server/vendored-conf/app.ini +538 -0
  43. package/server/vendored-conf/gitignore/Actionscript +11 -0
  44. package/server/vendored-conf/gitignore/Ada +5 -0
  45. package/server/vendored-conf/gitignore/Agda +1 -0
  46. package/server/vendored-conf/gitignore/Android +32 -0
  47. package/server/vendored-conf/gitignore/Anjuta +3 -0
  48. package/server/vendored-conf/gitignore/AppEngine +2 -0
  49. package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
  50. package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
  51. package/server/vendored-conf/gitignore/Archives +27 -0
  52. package/server/vendored-conf/gitignore/Autotools +14 -0
  53. package/server/vendored-conf/gitignore/BricxCC +4 -0
  54. package/server/vendored-conf/gitignore/C +32 -0
  55. package/server/vendored-conf/gitignore/C Sharp +108 -0
  56. package/server/vendored-conf/gitignore/C++ +28 -0
  57. package/server/vendored-conf/gitignore/CFWheels +12 -0
  58. package/server/vendored-conf/gitignore/CMake +6 -0
  59. package/server/vendored-conf/gitignore/CUDA +6 -0
  60. package/server/vendored-conf/gitignore/CakePHP +13 -0
  61. package/server/vendored-conf/gitignore/ChefCookbook +9 -0
  62. package/server/vendored-conf/gitignore/Cloud9 +3 -0
  63. package/server/vendored-conf/gitignore/CodeIgniter +6 -0
  64. package/server/vendored-conf/gitignore/CodeKit +3 -0
  65. package/server/vendored-conf/gitignore/CommonLisp +3 -0
  66. package/server/vendored-conf/gitignore/Composer +6 -0
  67. package/server/vendored-conf/gitignore/Concrete5 +3 -0
  68. package/server/vendored-conf/gitignore/Coq +3 -0
  69. package/server/vendored-conf/gitignore/CraftCMS +3 -0
  70. package/server/vendored-conf/gitignore/DM +5 -0
  71. package/server/vendored-conf/gitignore/Dart +15 -0
  72. package/server/vendored-conf/gitignore/DartEditor +2 -0
  73. package/server/vendored-conf/gitignore/Delphi +57 -0
  74. package/server/vendored-conf/gitignore/Dreamweaver +3 -0
  75. package/server/vendored-conf/gitignore/Drupal +36 -0
  76. package/server/vendored-conf/gitignore/EPiServer +4 -0
  77. package/server/vendored-conf/gitignore/Eagle +36 -0
  78. package/server/vendored-conf/gitignore/Eclipse +39 -0
  79. package/server/vendored-conf/gitignore/EiffelStudio +2 -0
  80. package/server/vendored-conf/gitignore/Elisp +5 -0
  81. package/server/vendored-conf/gitignore/Elixir +4 -0
  82. package/server/vendored-conf/gitignore/Emacs +32 -0
  83. package/server/vendored-conf/gitignore/Ensime +4 -0
  84. package/server/vendored-conf/gitignore/Erlang +10 -0
  85. package/server/vendored-conf/gitignore/Espresso +1 -0
  86. package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
  87. package/server/vendored-conf/gitignore/ExtJs +4 -0
  88. package/server/vendored-conf/gitignore/Fancy +2 -0
  89. package/server/vendored-conf/gitignore/Finale +13 -0
  90. package/server/vendored-conf/gitignore/FlexBuilder +3 -0
  91. package/server/vendored-conf/gitignore/ForceDotCom +4 -0
  92. package/server/vendored-conf/gitignore/FuelPHP +2 -0
  93. package/server/vendored-conf/gitignore/GWT +28 -0
  94. package/server/vendored-conf/gitignore/Gcov +5 -0
  95. package/server/vendored-conf/gitignore/GitBook +16 -0
  96. package/server/vendored-conf/gitignore/Go +24 -0
  97. package/server/vendored-conf/gitignore/Gradle +8 -0
  98. package/server/vendored-conf/gitignore/Grails +33 -0
  99. package/server/vendored-conf/gitignore/Haskell +16 -0
  100. package/server/vendored-conf/gitignore/IGORPro +5 -0
  101. package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
  102. package/server/vendored-conf/gitignore/Idris +2 -0
  103. package/server/vendored-conf/gitignore/JDeveloper +13 -0
  104. package/server/vendored-conf/gitignore/Java +12 -0
  105. package/server/vendored-conf/gitignore/Jboss +19 -0
  106. package/server/vendored-conf/gitignore/Jekyll +3 -0
  107. package/server/vendored-conf/gitignore/JetBrains +46 -0
  108. package/server/vendored-conf/gitignore/Joomla +525 -0
  109. package/server/vendored-conf/gitignore/KDevelop4 +2 -0
  110. package/server/vendored-conf/gitignore/Kate +3 -0
  111. package/server/vendored-conf/gitignore/KiCAD +13 -0
  112. package/server/vendored-conf/gitignore/Kohana +2 -0
  113. package/server/vendored-conf/gitignore/LabVIEW +16 -0
  114. package/server/vendored-conf/gitignore/Laravel +4 -0
  115. package/server/vendored-conf/gitignore/Lazarus +30 -0
  116. package/server/vendored-conf/gitignore/Leiningen +12 -0
  117. package/server/vendored-conf/gitignore/LemonStand +21 -0
  118. package/server/vendored-conf/gitignore/LibreOffice +2 -0
  119. package/server/vendored-conf/gitignore/Lilypond +6 -0
  120. package/server/vendored-conf/gitignore/Linux +7 -0
  121. package/server/vendored-conf/gitignore/Lithium +2 -0
  122. package/server/vendored-conf/gitignore/Lua +41 -0
  123. package/server/vendored-conf/gitignore/LyX +4 -0
  124. package/server/vendored-conf/gitignore/Magento +126 -0
  125. package/server/vendored-conf/gitignore/Matlab +16 -0
  126. package/server/vendored-conf/gitignore/Maven +9 -0
  127. package/server/vendored-conf/gitignore/Mercurial +6 -0
  128. package/server/vendored-conf/gitignore/Mercury +13 -0
  129. package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
  130. package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
  131. package/server/vendored-conf/gitignore/ModelSim +23 -0
  132. package/server/vendored-conf/gitignore/Momentics +8 -0
  133. package/server/vendored-conf/gitignore/MonoDevelop +8 -0
  134. package/server/vendored-conf/gitignore/Nanoc +10 -0
  135. package/server/vendored-conf/gitignore/NetBeans +8 -0
  136. package/server/vendored-conf/gitignore/Nim +1 -0
  137. package/server/vendored-conf/gitignore/Ninja +2 -0
  138. package/server/vendored-conf/gitignore/Node +28 -0
  139. package/server/vendored-conf/gitignore/NotepadPP +2 -0
  140. package/server/vendored-conf/gitignore/OCaml +20 -0
  141. package/server/vendored-conf/gitignore/Objective-C +43 -0
  142. package/server/vendored-conf/gitignore/Opa +13 -0
  143. package/server/vendored-conf/gitignore/OpenCart +11 -0
  144. package/server/vendored-conf/gitignore/OracleForms +8 -0
  145. package/server/vendored-conf/gitignore/Packer +5 -0
  146. package/server/vendored-conf/gitignore/Perl +20 -0
  147. package/server/vendored-conf/gitignore/Phalcon +2 -0
  148. package/server/vendored-conf/gitignore/PhpStorm +63 -0
  149. package/server/vendored-conf/gitignore/PlayFramework +17 -0
  150. package/server/vendored-conf/gitignore/Plone +18 -0
  151. package/server/vendored-conf/gitignore/Prestashop +25 -0
  152. package/server/vendored-conf/gitignore/Processing +7 -0
  153. package/server/vendored-conf/gitignore/Python +58 -0
  154. package/server/vendored-conf/gitignore/Qooxdoo +5 -0
  155. package/server/vendored-conf/gitignore/Qt +34 -0
  156. package/server/vendored-conf/gitignore/R +16 -0
  157. package/server/vendored-conf/gitignore/ROS +47 -0
  158. package/server/vendored-conf/gitignore/Rails +35 -0
  159. package/server/vendored-conf/gitignore/Redcar +1 -0
  160. package/server/vendored-conf/gitignore/Redis +3 -0
  161. package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
  162. package/server/vendored-conf/gitignore/Ruby +36 -0
  163. package/server/vendored-conf/gitignore/Rust +11 -0
  164. package/server/vendored-conf/gitignore/SBT +9 -0
  165. package/server/vendored-conf/gitignore/SCons +2 -0
  166. package/server/vendored-conf/gitignore/SVN +1 -0
  167. package/server/vendored-conf/gitignore/Sass +2 -0
  168. package/server/vendored-conf/gitignore/Scala +17 -0
  169. package/server/vendored-conf/gitignore/Scrivener +7 -0
  170. package/server/vendored-conf/gitignore/Sdcc +8 -0
  171. package/server/vendored-conf/gitignore/SeamGen +26 -0
  172. package/server/vendored-conf/gitignore/SketchUp +1 -0
  173. package/server/vendored-conf/gitignore/SlickEdit +11 -0
  174. package/server/vendored-conf/gitignore/Stella +12 -0
  175. package/server/vendored-conf/gitignore/SublimeText +14 -0
  176. package/server/vendored-conf/gitignore/SugarCRM +25 -0
  177. package/server/vendored-conf/gitignore/Swift +43 -0
  178. package/server/vendored-conf/gitignore/Symfony +37 -0
  179. package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
  180. package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
  181. package/server/vendored-conf/gitignore/Tags +14 -0
  182. package/server/vendored-conf/gitignore/TeX +136 -0
  183. package/server/vendored-conf/gitignore/TextMate +3 -0
  184. package/server/vendored-conf/gitignore/Textpattern +11 -0
  185. package/server/vendored-conf/gitignore/TortoiseGit +2 -0
  186. package/server/vendored-conf/gitignore/TurboGears2 +20 -0
  187. package/server/vendored-conf/gitignore/Typo3 +20 -0
  188. package/server/vendored-conf/gitignore/Umbraco +17 -0
  189. package/server/vendored-conf/gitignore/Unity +21 -0
  190. package/server/vendored-conf/gitignore/UnrealEngine +76 -0
  191. package/server/vendored-conf/gitignore/VVVV +6 -0
  192. package/server/vendored-conf/gitignore/Vagrant +1 -0
  193. package/server/vendored-conf/gitignore/Vim +6 -0
  194. package/server/vendored-conf/gitignore/VirtualEnv +10 -0
  195. package/server/vendored-conf/gitignore/VisualStudio +217 -0
  196. package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
  197. package/server/vendored-conf/gitignore/Waf +3 -0
  198. package/server/vendored-conf/gitignore/WebMethods +14 -0
  199. package/server/vendored-conf/gitignore/WebStorm +63 -0
  200. package/server/vendored-conf/gitignore/Windows +18 -0
  201. package/server/vendored-conf/gitignore/WordPress +18 -0
  202. package/server/vendored-conf/gitignore/Xcode +23 -0
  203. package/server/vendored-conf/gitignore/XilinxISE +57 -0
  204. package/server/vendored-conf/gitignore/Xojo +11 -0
  205. package/server/vendored-conf/gitignore/Yeoman +6 -0
  206. package/server/vendored-conf/gitignore/Yii +6 -0
  207. package/server/vendored-conf/gitignore/ZendFramework +20 -0
  208. package/server/vendored-conf/gitignore/Zephir +26 -0
  209. package/server/vendored-conf/gitignore/macOS +25 -0
  210. package/server/vendored-conf/label/Default +7 -0
  211. package/server/vendored-conf/license/Abstyles License +10 -0
  212. package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
  213. package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
  214. package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
  215. package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
  216. package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
  217. package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
  218. package/server/vendored-conf/license/Apache License 1.0 +19 -0
  219. package/server/vendored-conf/license/Apache License 1.1 +20 -0
  220. package/server/vendored-conf/license/Apache License 2.0 +72 -0
  221. package/server/vendored-conf/license/Artistic License 1.0 +48 -0
  222. package/server/vendored-conf/license/Artistic License 2.0 +84 -0
  223. package/server/vendored-conf/license/BSD 2-clause License +8 -0
  224. package/server/vendored-conf/license/BSD 3-clause License +10 -0
  225. package/server/vendored-conf/license/BSD 4-clause License +13 -0
  226. package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
  227. package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
  228. package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
  229. package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
  230. package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
  231. package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
  232. package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
  233. package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
  234. package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
  235. package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
  236. package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
  237. package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
  238. package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
  239. package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
  240. package/server/vendored-conf/license/ISC license +6 -0
  241. package/server/vendored-conf/license/MIT License +8 -0
  242. package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
  243. package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
  244. package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
  245. package/server/vendored-conf/readme/Default +3 -0
  246. package/src/dsh-host.js +168 -0
  247. package/src/index.js +682 -0
  248. package/src/runner.js +364 -0
package/src/runner.js ADDED
@@ -0,0 +1,364 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @weibaohui/dsh-git-server — ts-gogs 子进程运行器(宿主无关,可直测)。
5
+ *
6
+ * 职责:把 vendor/ts-gogs(Gogs 的 TypeScript 平替,构建产物随包分发)作为
7
+ * 受管子进程跑起来:
8
+ * 1. 把配置渲染成 app.ini 写进 <dataDir>/custom/conf/app.ini(INSTALL_LOCK
9
+ * 直接置位,跳过安装向导);
10
+ * 2. spawn `node dist/index.js`(GOGS_CUSTOM 指向上面的 custom 目录);
11
+ * 3. 就绪探测:轮询 http://host:port/ 直到 2xx/3xx(首连前就绪,防
12
+ * ECONNRESET 噪音),超时判失败并带上最后一段 stderr;
13
+ * 4. 崩溃自动拉起(退避 2s→30s);配置指纹变化由上层先 stop 再 start。
14
+ *
15
+ * 账户单一来源 = user-management:用户库路径通过环境变量传给 ts-gogs(
16
+ * - user-management:DSH_UM_USERS_FILE 指向 user-management 的 users.json,
17
+ * git HTTP Basic 与网页登录接受 UM 用户名密码(映射到本库管理员);
18
+ * - gogs(默认):ts-gogs 自己的账号/令牌体系。首次启动用
19
+ * DSH_BOOTSTRAP_ADMIN 播种管理员(用户库为空时)。
20
+ */
21
+
22
+ const { spawn } = require('node:child_process')
23
+ const fs = require('node:fs')
24
+ const path = require('node:path')
25
+ const crypto = require('node:crypto')
26
+ const os = require('node:os')
27
+
28
+ const PLUGIN_ID = 'dsh-git-server'
29
+ const SERVER_DIR = path.join(__dirname, '..', 'server')
30
+
31
+ const DEFAULTS = {
32
+ enabled: true,
33
+ host: '0.0.0.0', // 监听全网卡(局域网可访问);不再是设置项,写死
34
+ port: 3400,
35
+ dataDir: '', // 空 = dshHome()/dsh-git-server/data
36
+ adminPassword: '', // 播种的管理员密码(首次自动生成并持久化)
37
+ impersonateSecret: '', // 代理免登录用的会话铸造密钥(自动生成持久化)
38
+ }
39
+
40
+ const NUM_RANGES = { port: [1024, 65535] }
41
+
42
+ function dshHome() {
43
+ return process.env.DSH_HOME ? path.resolve(process.env.DSH_HOME) : path.join(require('node:os').homedir(), '.dsh')
44
+ }
45
+
46
+ function defaultDataDir() {
47
+ return path.join(dshHome(), PLUGIN_ID, 'data')
48
+ }
49
+
50
+ function resolveDataDir(dir) {
51
+ if (!dir || typeof dir !== 'string') return defaultDataDir()
52
+ const expanded = dir.startsWith('~') ? path.join(dshHome(), dir.slice(1)) : dir
53
+ return path.isAbsolute(expanded) ? expanded : path.join(dshHome(), expanded)
54
+ }
55
+
56
+ function generateSecret() {
57
+ return crypto.randomBytes(24).toString('hex')
58
+ }
59
+
60
+ function normalizeConfig(raw) {
61
+ const cfg = { ...DEFAULTS, ...(raw || {}) }
62
+ cfg.enabled = !!cfg.enabled
63
+ cfg.host = String(cfg.host || DEFAULTS.host)
64
+ cfg.port = Math.max(NUM_RANGES.port[0], Math.min(NUM_RANGES.port[1], Math.floor(Number(cfg.port) || DEFAULTS.port)))
65
+ cfg.dataDir = resolveDataDir(cfg.dataDir)
66
+ cfg.adminPassword = String(cfg.adminPassword || '')
67
+ cfg.impersonateSecret = String(cfg.impersonateSecret || '')
68
+ return cfg
69
+ }
70
+
71
+ function sanitizePatch(patch, current) {
72
+ const out = {}
73
+ if (patch == null || typeof patch !== 'object') return out
74
+ if (typeof patch.enabled === 'boolean') out.enabled = patch.enabled
75
+ if (patch.port !== undefined) {
76
+ const n = Math.floor(Number(patch.port))
77
+ if (Number.isFinite(n) && n >= NUM_RANGES.port[0] && n <= NUM_RANGES.port[1]) out.port = n
78
+ }
79
+ if (typeof patch.dataDir === 'string') out.dataDir = patch.dataDir.trim()
80
+ if (typeof patch.adminPassword === 'string') out.adminPassword = patch.adminPassword
81
+ if (typeof patch.impersonateSecret === 'string') out.impersonateSecret = patch.impersonateSecret
82
+ // 防呆:改动这些字段必须真的有变化,否则 reconciler 会空转重建
83
+ for (const k of Object.keys(out)) if (current && current[k] === out[k]) delete out[k]
84
+ return out
85
+ }
86
+
87
+ function serverFingerprint(cfg) {
88
+ return crypto.createHash('sha256')
89
+ .update(JSON.stringify({ enabled: cfg.enabled, host: cfg.host, port: cfg.port, dataDir: cfg.dataDir, adminPassword: cfg.adminPassword, impersonateSecret: cfg.impersonateSecret, vendor: vendorStamp() }))
90
+ .digest('hex')
91
+ }
92
+
93
+ let _vendorStamp = null
94
+ function vendorStamp() {
95
+ if (_vendorStamp) return _vendorStamp
96
+ try {
97
+ const st = fs.statSync(path.join(SERVER_DIR, 'dist', 'index.js'))
98
+ _vendorStamp = `${st.mtimeMs}:${st.size}`
99
+ } catch {
100
+ _vendorStamp = 'missing'
101
+ }
102
+ return _vendorStamp
103
+ }
104
+
105
+ /** 渲染并写入 app.ini(幂等;内容不变不写,避免无谓的 mtime 抖动)。 */
106
+ function writeAppIni(cfg) {
107
+ const customDir = path.join(cfg.dataDir, 'custom', 'conf')
108
+ fs.mkdirSync(customDir, { recursive: true })
109
+ fs.mkdirSync(path.join(cfg.dataDir, 'repositories'), { recursive: true })
110
+ fs.mkdirSync(path.join(cfg.dataDir, 'log'), { recursive: true })
111
+ const secretFile = path.join(cfg.dataDir, 'custom', 'secret-key.txt')
112
+ let secret = ''
113
+ try { secret = fs.readFileSync(secretFile, 'utf8').trim() } catch {}
114
+ if (!secret) {
115
+ secret = generateSecret()
116
+ fs.writeFileSync(secretFile, secret + '\n', { mode: 0o600 })
117
+ }
118
+ // dsh 变体:git 只走 HTTP(SSH 服务与系统 authorized_keys 全部移除)
119
+ const ini = [
120
+ `RUN_USER = ${os.userInfo().username}`,
121
+ 'RUN_MODE = prod',
122
+ '',
123
+ '[app]',
124
+ 'BRAND_NAME = dsh Git',
125
+ '',
126
+ '[auth]',
127
+ 'DISABLE_REGISTRATION = true',
128
+ '',
129
+ '[server]',
130
+ `HTTP_ADDR = ${cfg.host}`,
131
+ `HTTP_PORT = ${cfg.port}`,
132
+ `EXTERNAL_URL = http://${cfg.host === '0.0.0.0' || cfg.host === '::' ? '127.0.0.1' : cfg.host}:${cfg.port}/`,
133
+ 'DISABLE_SSH = true',
134
+ 'DISABLE_GRAVATAR = true',
135
+ '',
136
+ '[repository]',
137
+ `ROOT = ${path.join(cfg.dataDir, 'repositories')}`,
138
+ '',
139
+ '[database]',
140
+ `PATH = ${path.join(cfg.dataDir, 'gogs.db')}`,
141
+ '',
142
+ '[security]',
143
+ 'INSTALL_LOCK = true',
144
+ `SECRET_KEY = ${secret}`,
145
+ '',
146
+ '[log]',
147
+ 'MODE = file',
148
+ 'LEVEL = Info',
149
+ `ROOT_PATH = ${path.join(cfg.dataDir, 'log')}`,
150
+ '',
151
+ ].join('\n')
152
+ const file = path.join(customDir, 'app.ini')
153
+ try {
154
+ if (fs.readFileSync(file, 'utf8') === ini) return file
155
+ } catch {}
156
+ fs.writeFileSync(file, ini, { mode: 0o600 })
157
+ return file
158
+ }
159
+
160
+ let depsInstallInFlight = null
161
+
162
+ /**
163
+ * 依赖自检 + 自动安装。`dsh plugin add` 对 link: 包不装依赖、pnpm 默认
164
+ * 还会拦 better-sqlite3 的构建脚本,所以启动前自检关键原生依赖,缺失就在
165
+ * 插件目录自动跑一次 `npm install --omit=dev`(npm 默认执行安装脚本,
166
+ * better-sqlite3 拿预编译产物)。幂等:并发调用共享同一 in-flight。
167
+ */
168
+ function ensureDeps({ logger = () => {} } = {}) {
169
+ let probe
170
+ try {
171
+ const { createRequire } = require('node:module')
172
+ probe = createRequire(path.join(SERVER_DIR, 'dist', 'index.js'))
173
+ } catch {}
174
+ const critical = ['better-sqlite3', 'marked', 'ini', 'busboy']
175
+ const missing = []
176
+ for (const name of critical) {
177
+ try { probe && probe.resolve(name) } catch { missing.push(name) }
178
+ }
179
+ if (!missing.length) return { installed: false, missing: [] }
180
+ if (depsInstallInFlight) return depsInstallInFlight
181
+ logger(`检测到缺失依赖 ${missing.join(', ')},自动执行 npm install(首次可能需要 1-2 分钟)…`)
182
+ depsInstallInFlight = new Promise((resolve, reject) => {
183
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
184
+ const child = spawn(npm, ['install', '--omit=dev', '--no-audit', '--no-fund'], {
185
+ cwd: path.join(SERVER_DIR, '..', '..'),
186
+ stdio: ['ignore', 'pipe', 'pipe'],
187
+ })
188
+ let tail = ''
189
+ const collect = (d) => { tail = (tail + d.toString()).slice(-2000); logger(`[npm] ${d.toString().trimEnd()}`) }
190
+ child.stdout.on('data', collect)
191
+ child.stderr.on('data', collect)
192
+ const killer = setTimeout(() => child.kill('SIGKILL'), 300000)
193
+ child.once('exit', (code) => {
194
+ clearTimeout(killer)
195
+ depsInstallInFlight = null
196
+ // 装完复检
197
+ const still = []
198
+ for (const name of critical) {
199
+ try { probe && probe.resolve(name) } catch { still.push(name) }
200
+ }
201
+ if (code === 0 && !still.length) resolve({ installed: true, missing: [] })
202
+ else reject(new Error(`依赖自动安装失败(exit=${code}${still.length ? ',仍缺: ' + still.join(',') : ''})\n${tail.slice(-600)}`))
203
+ })
204
+ })
205
+ return depsInstallInFlight
206
+ }
207
+
208
+ /** 按实例标签清理本数据目录的历史孤儿子进程(不同实例互不影响)。 */
209
+ function killOrphanChildren(tag, logger) {
210
+ try {
211
+ const marker = path.join(SERVER_DIR, 'dist', 'index.js')
212
+ const needle = tag ? `--dsh-instance=${tag}` : null
213
+ if (!needle) return
214
+ const out = require('node:child_process')
215
+ .execSync(`ps ax -o pid=,command= | grep -F '${marker}' | grep -v grep || true`, { shell: '/bin/bash' })
216
+ .toString()
217
+ for (const line of out.split('\n')) {
218
+ if (!line.includes(needle)) continue
219
+ const pid = parseInt(line.trim().split(/\s+/)[0], 10)
220
+ if (pid && pid !== process.pid) {
221
+ try { process.kill(pid, 'SIGTERM'); logger(`清理历史孤儿子进程 pid=${pid}`) } catch {}
222
+ }
223
+ }
224
+ } catch {}
225
+ }
226
+
227
+ /** 定位 user-management 插件的 store 模块(dsh 服务注入)。
228
+ * 优先 web profile 的安装(软链指向真实仓库);找不到返回空串——server
229
+ * 侧据此回退本地兜底账号。DSH_UM_STORE_PATH 环境变量可显式覆盖。 */
230
+ function locateUmStore() {
231
+ if (process.env.DSH_UM_STORE_PATH) return process.env.DSH_UM_STORE_PATH
232
+ const candidates = [
233
+ path.join(dshHome(), 'profiles', 'web', 'node_modules', '@weibaohui', 'user-management', 'src', 'store.js'),
234
+ ]
235
+ for (const c of candidates) {
236
+ try { fs.statSync(c); return c } catch {}
237
+ }
238
+ return ''
239
+ }
240
+
241
+ /** 实例标签:每个数据目录一个持久随机标签(<dataDir>/custom/instance-tag)。 */
242
+ function ensureInstanceTag(cfg) {
243
+ const file = path.join(cfg.dataDir, 'custom', 'instance-tag')
244
+ try { return fs.readFileSync(file, 'utf8').trim() } catch {}
245
+ const tag = crypto.randomBytes(8).toString('hex')
246
+ fs.mkdirSync(path.dirname(file), { recursive: true })
247
+ fs.writeFileSync(file, tag + '\n', { mode: 0o600 })
248
+ return tag
249
+ }
250
+
251
+ /**
252
+ * 启动 ts-gogs 子进程。返回句柄 { pid, stop() }。
253
+ * 就绪前抛错(带 stderr 尾部);就绪判定 = GET / 返回任意状态(TS 服务
254
+ * 起来后即使 404/302 也代表监听器在线)。
255
+ */
256
+ async function start(cfg, options = {}) {
257
+ const logger = options.logger || (() => {})
258
+ const instanceTag = ensureInstanceTag(cfg)
259
+ killOrphanChildren(instanceTag, logger)
260
+ await new Promise((r) => setTimeout(r, 600)) // 等端口释放
261
+ await ensureDeps({ logger })
262
+ // 偶发:子进程刚起来就收到未知来源的信号——按标签清理后重试一次
263
+ try {
264
+ return await spawnAndReady(cfg, { ...options, instanceTag })
265
+ } catch (e) {
266
+ if (!/进程退出|就绪探测超时/.test(String((e && e.message) || e))) throw e
267
+ logger('Git 服务器启动中断,清理后重试一次…')
268
+ killOrphanChildren(instanceTag, logger)
269
+ await new Promise((r) => setTimeout(r, 1200))
270
+ return spawnAndReady(cfg, { ...options, instanceTag })
271
+ }
272
+ }
273
+
274
+ async function spawnAndReady(cfg, { logger = () => {}, onExit, instanceTag } = {}) {
275
+ const appIni = writeAppIni(cfg)
276
+ const entry = path.join(SERVER_DIR, 'dist', 'index.js')
277
+ if (!fs.existsSync(entry)) throw new Error(`vendor/ts-gogs 构建产物缺失: ${entry}`)
278
+ const env = {
279
+ ...process.env,
280
+ GOGS_CUSTOM: path.join(cfg.dataDir, 'custom'),
281
+ DSH_BOOTSTRAP_ADMIN: `root:${cfg.adminPassword || generateSecret()}`,
282
+ }
283
+ if (cfg.adminPassword) env.DSH_ADMIN_PASSWORD = cfg.adminPassword
284
+ if (cfg.impersonateSecret) env.DSH_IMPERSONATE_SECRET = cfg.impersonateSecret
285
+ // 账户单一来源:注入 user-management service(dsh profile 安装的 store 模块,
286
+ // 与 UM 网关同一代码路径);server 侧用它直接做登录校验,密码永不同步
287
+ env.DSH_UM_STORE_PATH = locateUmStore()
288
+ if (!env.DSH_HOME) env.DSH_HOME = dshHome()
289
+
290
+ const spawnArgs = instanceTag ? [entry, `--dsh-instance=${instanceTag}`] : [entry]
291
+ const child = spawn(process.execPath, spawnArgs, { cwd: SERVER_DIR, env, stdio: ['ignore', 'pipe', 'pipe'] })
292
+ let stderrTail = ''
293
+ child.stderr.on('data', (d) => {
294
+ stderrTail = (stderrTail + d.toString()).slice(-4000)
295
+ logger(`[gogs] ${d.toString().trimEnd()}`)
296
+ })
297
+ child.stdout.on('data', (d) => logger(`[gogs] ${d.toString().trimEnd()}`))
298
+
299
+ const exited = new Promise((resolve) => child.once('exit', (code, sig) => resolve({ code, sig })))
300
+
301
+ // 就绪探测:最长 30s;进程先退则立即失败
302
+ const deadline = Date.now() + 30000
303
+ let ready = false
304
+ let lastErr = ''
305
+ while (Date.now() < deadline) {
306
+ const gone = await Promise.race([exited.then((e) => e || { code: 0 }), new Promise((r) => setTimeout(() => r(null), 500))])
307
+ if (gone) {
308
+ throw new Error(`ts-gogs 进程退出 code=${gone.code}\n${stderrTail.slice(-800)}`)
309
+ }
310
+ try {
311
+ const res = await fetch(`http://${cfg.host === '0.0.0.0' || cfg.host === '::' ? '127.0.0.1' : cfg.host}:${cfg.port}/`, { redirect: 'manual' })
312
+ if (res.status > 0) { ready = true; break }
313
+ } catch (e) {
314
+ lastErr = (e && e.message) || String(e)
315
+ }
316
+ }
317
+ if (!ready) {
318
+ child.kill('SIGTERM')
319
+ throw new Error(`ts-gogs 就绪探测超时(30s):${lastErr}\n${stderrTail.slice(-800)}`)
320
+ }
321
+
322
+ let stopping = false
323
+ const parentExit = () => { try { child.kill('SIGKILL') } catch {} }
324
+ process.once('exit', parentExit)
325
+ process.once('SIGTERM', parentExit)
326
+ process.once('SIGINT', parentExit)
327
+ const handle = {
328
+ pid: child.pid,
329
+ async stop() {
330
+ if (stopping) return
331
+ stopping = true
332
+ process.removeListener('exit', parentExit)
333
+ process.removeListener('SIGTERM', parentExit)
334
+ process.removeListener('SIGINT', parentExit)
335
+ child.removeAllListeners('exit')
336
+ child.kill('SIGTERM')
337
+ const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, 5000)
338
+ await new Promise((resolve) => child.once('exit', () => { clearTimeout(t); resolve() }))
339
+ },
340
+ onExit(cb) {
341
+ child.once('exit', (code, sig) => { if (!stopping && cb) cb({ code, sig, stderrTail }) })
342
+ },
343
+ appIni,
344
+ }
345
+ return handle
346
+ }
347
+
348
+ module.exports = {
349
+ ensureDeps,
350
+ locateUmStore,
351
+ PLUGIN_ID,
352
+ DEFAULTS,
353
+ NUM_RANGES,
354
+ SERVER_DIR,
355
+ dshHome,
356
+ defaultDataDir,
357
+ resolveDataDir,
358
+ normalizeConfig,
359
+ sanitizePatch,
360
+ serverFingerprint,
361
+ writeAppIni,
362
+ start,
363
+ generateSecret,
364
+ }