@wwkit/opm 1.0.1

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 (62) hide show
  1. package/README.md +381 -0
  2. package/bin/index.js +9 -0
  3. package/docs/managers/apt.md +45 -0
  4. package/docs/managers/brew.md +92 -0
  5. package/docs/managers/bun.md +50 -0
  6. package/docs/managers/cft.md +57 -0
  7. package/docs/managers/composer.md +96 -0
  8. package/docs/managers/config.md +32 -0
  9. package/docs/managers/dnf.md +46 -0
  10. package/docs/managers/node.md +49 -0
  11. package/docs/managers/npm.md +100 -0
  12. package/docs/managers/nvm.md +34 -0
  13. package/docs/managers/php.md +48 -0
  14. package/docs/managers/phpenv.md +34 -0
  15. package/docs/managers/pip.md +98 -0
  16. package/docs/managers/python.md +53 -0
  17. package/docs/managers/uv.md +27 -0
  18. package/docs/opm.html +570 -0
  19. package/docs/packages/opencode.md +41 -0
  20. package/package.json +51 -0
  21. package/scripts/postinstall.js +7 -0
  22. package/src/binaries/base.js +508 -0
  23. package/src/binaries/cft/index.js +35 -0
  24. package/src/binaries/depchecker.js +172 -0
  25. package/src/binaries/exefinder.js +110 -0
  26. package/src/binaries/platform.js +17 -0
  27. package/src/cli/doc.js +335 -0
  28. package/src/cli/groups/config.js +78 -0
  29. package/src/cli/groups/ensure.js +96 -0
  30. package/src/cli/groups/homebrew.js +73 -0
  31. package/src/cli/groups/nvm.js +73 -0
  32. package/src/cli/groups/package.js +514 -0
  33. package/src/cli/groups/phpenv.js +89 -0
  34. package/src/cli/groups/uv.js +73 -0
  35. package/src/cli/groups/version-manager.js +186 -0
  36. package/src/cli/helpers/args.js +49 -0
  37. package/src/cli/index.js +129 -0
  38. package/src/config.js +133 -0
  39. package/src/config.json5 +158 -0
  40. package/src/formatter.js +13 -0
  41. package/src/harnesses/base.js +205 -0
  42. package/src/harnesses/opencode/clear.js +239 -0
  43. package/src/harnesses/opencode/db.js +39 -0
  44. package/src/harnesses/opencode/index.js +46 -0
  45. package/src/harnesses/opencode/sqlite.js +200 -0
  46. package/src/index.js +46 -0
  47. package/src/managers/apt.js +377 -0
  48. package/src/managers/base.js +167 -0
  49. package/src/managers/brew.js +374 -0
  50. package/src/managers/bun.js +342 -0
  51. package/src/managers/composer.js +280 -0
  52. package/src/managers/dnf.js +370 -0
  53. package/src/managers/http.js +432 -0
  54. package/src/managers/node.js +150 -0
  55. package/src/managers/npm.js +473 -0
  56. package/src/managers/php.js +157 -0
  57. package/src/managers/pip.js +555 -0
  58. package/src/managers/python.js +180 -0
  59. package/src/managers/runtime.js +309 -0
  60. package/src/managers/winget.js +268 -0
  61. package/src/presets.js +24 -0
  62. package/src/proxy-presets.js +12 -0
@@ -0,0 +1,172 @@
1
+ import { execSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import process from 'node:process'
4
+
5
+ const APT_MAP = {
6
+ 'libnss3.so': 'libnss3',
7
+ 'libatk-1.0.so.0': 'libatk1.0-0t64',
8
+ 'libatk-bridge-2.0.so.0': 'libatk-bridge2.0-0t64',
9
+ 'libcups.so.2': 'libcups2t64',
10
+ 'libdrm.so.2': 'libdrm2',
11
+ 'libgbm.so.1': 'libgbm1',
12
+ 'libxkbcommon.so.0': 'libxkbcommon0',
13
+ 'libXcomposite.so.1': 'libxcomposite1',
14
+ 'libXdamage.so.1': 'libxdamage1',
15
+ 'libXfixes.so.3': 'libxfixes3',
16
+ 'libXrandr.so.2': 'libxrandr2',
17
+ 'libasound.so.2': 'libasound2t64',
18
+ 'libpango-1.0.so.0': 'libpango-1.0-0',
19
+ 'libcairo.so.2': 'libcairo2',
20
+ 'libpangoft2-1.0.so.0': 'libpangoft2-1.0-0',
21
+ 'libharfbuzz.so.0': 'libharfbuzz0b',
22
+ 'libXss.so.1': 'libxss1',
23
+ 'libatspi.so.0': 'libatspi2.0-0t64',
24
+ 'libxcb.so.1': 'libxcb1',
25
+ 'libX11.so.6': 'libx11-6',
26
+ 'libXext.so.6': 'libxext6',
27
+ 'libnssutil3.so': 'libnss3',
28
+ 'libsmime3.so': 'libnss3',
29
+ 'libnspr4.so': 'libnspr4',
30
+ 'libdbus-1.so.3': 'libdbus-1-3',
31
+ 'libexpat.so.1': 'libexpat1',
32
+ 'libxrender.so.1': 'libxrender1',
33
+ 'libglib-2.0.so.0': 'libglib2.0-0',
34
+ 'libgobject-2.0.so.0': 'libglib2.0-0',
35
+ }
36
+
37
+ const DNF_MAP = {
38
+ 'libnss3.so': 'nss',
39
+ 'libatk-1.0.so.0': 'atk',
40
+ 'libatk-bridge-2.0.so.0': 'at-spi2-atk',
41
+ 'libcups.so.2': 'libcups',
42
+ 'libdrm.so.2': 'libdrm',
43
+ 'libgbm.so.1': 'mesa-libgbm',
44
+ 'libxkbcommon.so.0': 'libxkbcommon',
45
+ 'libXcomposite.so.1': 'libXcomposite',
46
+ 'libXdamage.so.1': 'libXdamage',
47
+ 'libXfixes.so.3': 'libXfixes',
48
+ 'libXrandr.so.2': 'libXrandr',
49
+ 'libasound.so.2': 'alsa-lib',
50
+ 'libpango-1.0.so.0': 'pango',
51
+ 'libcairo.so.2': 'cairo',
52
+ 'libpangoft2-1.0.so.0': 'pango',
53
+ 'libharfbuzz.so.0': 'harfbuzz',
54
+ 'libXss.so.1': 'libXScrnSaver',
55
+ 'libatspi.so.0': 'at-spi2-core',
56
+ 'libxcb.so.1': 'libxcb',
57
+ 'libX11.so.6': 'libX11',
58
+ 'libXext.so.6': 'libXext',
59
+ 'libnssutil3.so': 'nss',
60
+ 'libsmime3.so': 'nss',
61
+ 'libnspr4.so': 'nspr',
62
+ 'libdbus-1.so.3': 'dbus-libs',
63
+ 'libexpat.so.1': 'expat',
64
+ 'libxrender.so.1': 'libXrender',
65
+ 'libglib-2.0.so.0': 'glib2',
66
+ 'libgobject-2.0.so.0': 'glib2',
67
+ }
68
+
69
+ export function isSupported() {
70
+ return process.platform === 'linux'
71
+ }
72
+
73
+ export function check(binaryPath) {
74
+ if (!isSupported()) return []
75
+ if (!binaryPath || !fs.existsSync(binaryPath)) return []
76
+ try {
77
+ execSync('which ldd', { stdio: 'pipe' })
78
+ } catch {
79
+ return []
80
+ }
81
+ let result
82
+ try {
83
+ result = execSync(`ldd "${binaryPath}"`, { stdio: 'pipe', timeout: 20000 })
84
+ } catch {
85
+ return []
86
+ }
87
+ const stdout = result.stdout?.toString() || ''
88
+ const missing = []
89
+ for (const line of stdout.split('\n')) {
90
+ const m = line.match(/^\s*(\S+)\s+=>\s+not found/)
91
+ if (m) missing.push(m[1])
92
+ }
93
+ return missing
94
+ }
95
+
96
+ export function mapToPackages(libs, packageManager) {
97
+ if (packageManager !== 'apt' && packageManager !== 'dnf') {
98
+ return { packages: [], unknown: [...libs] }
99
+ }
100
+ const table = packageManager === 'apt' ? APT_MAP : DNF_MAP
101
+ const packages = []
102
+ const unknown = []
103
+ const seen = new Set()
104
+ for (const lib of libs) {
105
+ const pkg = table[lib]
106
+ if (pkg && !seen.has(pkg)) {
107
+ seen.add(pkg)
108
+ packages.push(pkg)
109
+ } else if (!pkg) {
110
+ unknown.push(lib)
111
+ }
112
+ }
113
+ return { packages, unknown }
114
+ }
115
+
116
+ export function fallbackPackages(packages, packageManager) {
117
+ if (packageManager !== 'apt') return []
118
+ const fb = packages.map((p) => p.replace(/t64$/, ''))
119
+ return fb.some((p, i) => p !== packages[i]) ? fb : []
120
+ }
121
+
122
+ function runInstall(packages, packageManager) {
123
+ const tool = packageManager === 'apt' ? 'apt-get' : 'dnf'
124
+ const args = [tool, 'install', '-y', ...packages]
125
+ const cmd = process.getuid && process.getuid() === 0 ? args : ['sudo', ...args]
126
+ try {
127
+ execSync(cmd.join(' '), { stdio: 'pipe', timeout: 600000 })
128
+ return true
129
+ } catch {
130
+ return false
131
+ }
132
+ }
133
+
134
+ export function install(packages, packageManager) {
135
+ if (!packages.length) return true
136
+ if (packageManager !== 'apt' && packageManager !== 'dnf') return false
137
+ if (runInstall(packages, packageManager)) return true
138
+ const fb = fallbackPackages(packages, packageManager)
139
+ return fb.length ? runInstall(fb, packageManager) : false
140
+ }
141
+
142
+ export function detectPackageManager() {
143
+ try { execSync('which apt-get', { stdio: 'pipe' }); return 'apt' } catch {}
144
+ try { execSync('which dnf', { stdio: 'pipe' }); return 'dnf' } catch {}
145
+ return null
146
+ }
147
+
148
+ export function verify(binaryPath) {
149
+ if (!isSupported()) {
150
+ return { missing: [], packages: [], unknown: [], ok: true }
151
+ }
152
+ const packageManager = detectPackageManager()
153
+ const missing = check(binaryPath)
154
+ const result = { missing, packages: [], unknown: [], ok: !missing.length }
155
+ if (!missing.length) return result
156
+
157
+ const { packages, unknown } = mapToPackages(missing, packageManager)
158
+ result.packages = packages
159
+ result.unknown = unknown
160
+
161
+ if (packages.length) {
162
+ if (install(packages, packageManager)) {
163
+ const stillMissing = check(binaryPath)
164
+ result.ok = !stillMissing.length
165
+ } else {
166
+ result.ok = false
167
+ }
168
+ } else {
169
+ result.ok = false
170
+ }
171
+ return result
172
+ }
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ function getPossibleNames(baseName) {
6
+ const names = new Set()
7
+ if (baseName === 'chrome') {
8
+ ['chrome', 'google chrome', 'google chrome for testing'].forEach(n => {
9
+ names.add(n.toLowerCase())
10
+ names.add(n.toLowerCase() + '.exe')
11
+ })
12
+ } else if (baseName === 'chromedriver') {
13
+ names.add('chromedriver')
14
+ names.add('chromedriver.exe')
15
+ } else {
16
+ names.add(baseName.toLowerCase())
17
+ names.add(baseName.toLowerCase() + '.exe')
18
+ }
19
+ return names
20
+ }
21
+
22
+ function findFallback(directory, baseName, isMacos) {
23
+ if (baseName === 'chrome' && isMacos) {
24
+ const paths = [
25
+ path.join(directory, 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'),
26
+ path.join(directory, 'Chrome.app', 'Contents', 'MacOS', 'Chrome'),
27
+ path.join(directory, 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
28
+ ]
29
+ for (const p of paths) {
30
+ if (fs.existsSync(p)) return p
31
+ }
32
+ } else if (baseName === 'chromedriver') {
33
+ const paths = [
34
+ path.join(directory, 'chromedriver'),
35
+ path.join(directory, 'chromedriver.exe'),
36
+ ]
37
+ for (const p of paths) {
38
+ if (fs.existsSync(p)) return p
39
+ }
40
+ }
41
+ return null
42
+ }
43
+
44
+ function setAppPermissions(appBundlePath) {
45
+ if (!fs.existsSync(appBundlePath)) return
46
+ for (const entry of fs.readdirSync(appBundlePath, { withFileTypes: true })) {
47
+ const fullPath = path.join(appBundlePath, entry.name)
48
+ if (entry.isDirectory()) {
49
+ setAppPermissions(fullPath)
50
+ } else {
51
+ try { fs.chmodSync(fullPath, 0o755) } catch {}
52
+ }
53
+ }
54
+ }
55
+
56
+ function ensureExecutable(filePath, isMacos = false, baseName = '') {
57
+ if (process.platform === 'win32') return filePath
58
+ try {
59
+ fs.accessSync(filePath, fs.constants.X_OK)
60
+ return filePath
61
+ } catch {}
62
+ try {
63
+ fs.chmodSync(filePath, 0o755)
64
+ if (isMacos && baseName === 'chrome') {
65
+ const parts = filePath.split('/Contents/MacOS/')
66
+ if (parts.length > 1 && parts[0].endsWith('.app')) {
67
+ setAppPermissions(parts[0])
68
+ }
69
+ }
70
+ } catch {}
71
+ return filePath
72
+ }
73
+
74
+ export function find(directory, baseName) {
75
+ const isMacos = process.platform === 'darwin'
76
+
77
+ if (!fs.existsSync(directory)) {
78
+ throw new Error(`Directory not found: ${directory}`)
79
+ }
80
+
81
+ if (fs.statSync(directory).isFile()) {
82
+ try {
83
+ fs.accessSync(directory, fs.constants.X_OK)
84
+ return ensureExecutable(directory)
85
+ } catch {}
86
+ }
87
+
88
+ const possibleNames = getPossibleNames(baseName)
89
+
90
+ function walkDir(dir) {
91
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
92
+ const fullPath = path.join(dir, entry.name)
93
+ if (entry.isDirectory()) {
94
+ const result = walkDir(fullPath)
95
+ if (result) return result
96
+ } else if (possibleNames.has(entry.name.toLowerCase())) {
97
+ return fullPath
98
+ }
99
+ }
100
+ return null
101
+ }
102
+
103
+ const found = walkDir(directory)
104
+ if (found) return ensureExecutable(found, isMacos, baseName)
105
+
106
+ const fallback = findFallback(directory, baseName, isMacos)
107
+ if (fallback) return ensureExecutable(fallback, isMacos, baseName)
108
+
109
+ throw new Error(`Executable not found: ${baseName} in ${directory}`)
110
+ }
@@ -0,0 +1,17 @@
1
+ import process from 'node:process'
2
+
3
+ export const OS_TYPES = ['linux64', 'win64', 'mac-x64', 'mac-arm64']
4
+
5
+ export function getOs() {
6
+ const platform = process.platform
7
+ const arch = process.arch
8
+ if (platform === 'win32') return 'win64'
9
+ if (platform === 'darwin') return arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
10
+ if (platform === 'linux') {
11
+ if (arch !== 'x64') {
12
+ throw new Error(`Chrome for Testing 不提供 linux-${arch} 构建(上游仅支持 linux64/x86_64)。请改用系统 Chromium:apt/dnf install chromium chromium-driver`)
13
+ }
14
+ return 'linux64'
15
+ }
16
+ throw new Error(`Unsupported platform: ${platform}`)
17
+ }
package/src/cli/doc.js ADDED
@@ -0,0 +1,335 @@
1
+ /**
2
+ * 文档生成器 — 将 docs/ 下的 markdown 转为单个 HTML(左侧菜单 + 右侧内容),
3
+ * 写入 docs/opm.html 并自动用浏览器打开。
4
+ *
5
+ * 无外部依赖:markdown → HTML 转换为最小化内联实现(标题/表格/代码块/列表/段落)。
6
+ * 发布时由 prepublishOnly 调用 generateDocHtml({ open: false }) 重新生成。
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import os from 'node:os'
12
+ import { execSync } from 'node:child_process'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
+ const DOCS_DIR = path.resolve(__dirname, '..', '..', 'docs')
17
+ const OUTPUT = path.join(DOCS_DIR, 'opm.html')
18
+
19
+ /**
20
+ * 收集 docs/ 下所有 .md 文件(递归),返回 [{ name, relPath, content }]
21
+ * @returns {{ name: string, relPath: string, content: string }[]}
22
+ */
23
+ function collectDocs() {
24
+ const result = []
25
+ function walk(dir, prefix) {
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
27
+ for (const e of entries) {
28
+ if (e.name.startsWith('.') || e.name === 'opm.html') continue
29
+ const full = path.join(dir, e.name)
30
+ if (e.isDirectory()) {
31
+ walk(full, prefix ? `${prefix}/${e.name}` : e.name)
32
+ } else if (e.name.endsWith('.md')) {
33
+ const base = e.name.replace(/\.md$/, '')
34
+ result.push({
35
+ name: prefix ? `${prefix}/${base}` : base,
36
+ relPath: prefix ? `${prefix}/${base}` : base,
37
+ content: fs.readFileSync(full, 'utf8'),
38
+ })
39
+ }
40
+ }
41
+ }
42
+ walk(DOCS_DIR, '')
43
+ return result
44
+ }
45
+
46
+ /** 按未转义的 | 分割表格行(\| 视为字面量) */
47
+ function splitPipes(line) {
48
+ const parts = []
49
+ let current = ''
50
+ for (let j = 0; j < line.length; j++) {
51
+ if (line[j] === '|' && j > 0 && line[j - 1] === '\\') {
52
+ current = current.slice(0, -1) + '|'
53
+ } else if (line[j] === '|') {
54
+ parts.push(current)
55
+ current = ''
56
+ } else {
57
+ current += line[j]
58
+ }
59
+ }
60
+ parts.push(current)
61
+ return parts.map((s) => s.trim()).filter(Boolean)
62
+ }
63
+
64
+ /**
65
+ * 极简 markdown → HTML 转换(支持标题/表格/代码块/列表/段落/加粗/行内代码)
66
+ * @param {string} md
67
+ * @returns {string}
68
+ */
69
+ function mdToHtml(md) {
70
+ const lines = md.split('\n')
71
+ const out = []
72
+ let i = 0
73
+ while (i < lines.length) {
74
+ const line = lines[i]
75
+ // 代码块
76
+ if (line.trim().startsWith('```')) {
77
+ const lang = line.trim().slice(3)
78
+ const code = []
79
+ i++
80
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
81
+ code.push(lines[i])
82
+ i++
83
+ }
84
+ i++
85
+ out.push(`<pre><code class="lang-${lang}">${esc(code.join('\n'))}</code></pre>`)
86
+ continue
87
+ }
88
+ // 表格
89
+ if (line.includes('|') && i + 1 < lines.length && lines[i + 1].includes('---')) {
90
+ const headers = splitPipes(line)
91
+ i += 2
92
+ const rows = []
93
+ while (i < lines.length && lines[i].includes('|')) {
94
+ rows.push(splitPipes(lines[i]))
95
+ i++
96
+ }
97
+ const thead = `<thead><tr>${headers.map((h) => `<th>${inline(h)}</th>`).join('')}</tr></thead>`
98
+ const tbody = `<tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</tbody>`
99
+ out.push(`<table>${thead}${tbody}</table>`)
100
+ continue
101
+ }
102
+ // 标题
103
+ const h = line.match(/^(#{1,6})\s+(.*)/)
104
+ if (h) {
105
+ const level = h[1].length
106
+ out.push(`<h${level}>${inline(h[2])}</h${level}>`)
107
+ i++
108
+ continue
109
+ }
110
+ // 无序列表
111
+ if (line.match(/^\s*[-*]\s+/)) {
112
+ const items = []
113
+ while (i < lines.length && lines[i].match(/^\s*[-*]\s+/)) {
114
+ items.push(`<li>${inline(lines[i].replace(/^\s*[-*]\s+/, ''))}</li>`)
115
+ i++
116
+ }
117
+ out.push(`<ul>${items.join('')}</ul>`)
118
+ continue
119
+ }
120
+ // 引用
121
+ if (line.startsWith('> ')) {
122
+ const quotes = []
123
+ while (i < lines.length && lines[i].startsWith('> ')) {
124
+ quotes.push(inline(lines[i].slice(2)))
125
+ i++
126
+ }
127
+ out.push(`<blockquote>${quotes.join('<br>')}</blockquote>`)
128
+ continue
129
+ }
130
+ // 空行
131
+ if (line.trim() === '') {
132
+ i++
133
+ continue
134
+ }
135
+ // 段落
136
+ out.push(`<p>${inline(line)}</p>`)
137
+ i++
138
+ }
139
+ return out.join('\n')
140
+ }
141
+
142
+ /** HTML 转义 */
143
+ function esc(s) {
144
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
145
+ }
146
+
147
+ /** 行内格式化(加粗、行内代码、链接) */
148
+ function inline(s) {
149
+ return esc(s)
150
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
151
+ .replace(/`(.+?)`/g, '<code>$1</code>')
152
+ .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
153
+ }
154
+
155
+ /**
156
+ * 生成 docs/opm.html 并在浏览器打开
157
+ * @param {{ open?: boolean }} [opts] — open 默认 true;发布构建时传 false
158
+ */
159
+ export function generateDocHtml(opts = {}) {
160
+ const open = opts.open !== false
161
+ const docs = collectDocs()
162
+ if (docs.length === 0) {
163
+ console.error('No .md files found under docs/')
164
+ process.exit(1)
165
+ }
166
+
167
+ const sections = {}
168
+ for (const doc of docs) {
169
+ const parts = doc.relPath.split('/')
170
+ const category = parts.length > 1 ? parts[0] : 'root'
171
+ if (!sections[category]) sections[category] = []
172
+ sections[category].push(doc)
173
+ }
174
+
175
+ const menuItems = []
176
+ const contentDivs = []
177
+ let first = true
178
+
179
+ for (const [category, items] of Object.entries(sections)) {
180
+ menuItems.push(`<div class="menu-category">${esc(category)}</div>`)
181
+ for (const doc of items) {
182
+ const id = doc.relPath.replace(/[^a-zA-Z0-9]/g, '_')
183
+ const label = parts_last(doc.relPath)
184
+ menuItems.push(`<a class="menu-item" href="#" onclick="show('${id}');return false;">${esc(label)}</a>`)
185
+ contentDivs.push(`<div id="${id}" class="doc-content" style="display:${first ? 'block' : 'none'};">${mdToHtml(doc.content)}</div>`)
186
+ first = false
187
+ }
188
+ }
189
+
190
+ const html = `<!DOCTYPE html>
191
+ <html lang="zh-CN">
192
+ <head>
193
+ <meta charset="UTF-8">
194
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
195
+ <title>OPM — Once Package Manager</title>
196
+ <style>
197
+ :root {
198
+ --bg: #ffffff;
199
+ --bg-soft: #f8f9fb;
200
+ --bg-sidebar: #1a1b26;
201
+ --bg-header: #1a1b26;
202
+ --text: #1e2030;
203
+ --text-soft: #6b7290;
204
+ --text-sidebar: #a9b1d6;
205
+ --text-sidebar-dim: #565f89;
206
+ --accent: #7aa2f7;
207
+ --accent-soft: #b4f9f8;
208
+ --border: #e2e4ee;
209
+ --code-bg: #1a1b26;
210
+ --code-text: #c0caf5;
211
+ }
212
+ * { margin: 0; padding: 0; box-sizing: border-box; }
213
+ html, body { height: 100%; }
214
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif; display: flex; flex-direction: column; height: 100vh; }
215
+
216
+ /* ---- header ---- */
217
+ #header {
218
+ height: 52px; flex-shrink: 0;
219
+ background: var(--bg-header);
220
+ display: flex; align-items: center; gap: 12px;
221
+ padding: 0 24px;
222
+ box-shadow: 0 1px 4px rgba(0,0,0,.15);
223
+ z-index: 10;
224
+ }
225
+ #header .logo {
226
+ font-size: 13px; font-weight: 700; letter-spacing: 1px;
227
+ color: #1a1b26; background: var(--accent);
228
+ padding: 3px 10px; border-radius: 4px;
229
+ }
230
+ #header .title { color: #c0caf5; font-size: 15px; font-weight: 500; }
231
+ #header .title .sub { color: var(--text-sidebar-dim); font-size: 13px; margin-left: 8px; }
232
+
233
+ /* ---- body layout ---- */
234
+ #body-row { display: flex; flex: 1; overflow: hidden; }
235
+
236
+ /* ---- sidebar ---- */
237
+ #sidebar { width: 220px; background: var(--bg-sidebar); overflow-y: auto; padding: 8px 0; flex-shrink: 0; }
238
+ .menu-category { padding: 10px 16px 4px; font-size: 10px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-sidebar-dim); }
239
+ .menu-item { display: block; padding: 5px 20px; color: var(--text-sidebar); text-decoration: none; font-size: 13px; border-left: 2px solid transparent; transition: background .15s, border-color .15s; }
240
+ .menu-item:hover { background: rgba(122,162,247,.08); }
241
+ .menu-item.active { border-left-color: var(--accent); color: var(--accent); background: rgba(122,162,247,.06); }
242
+
243
+ /* ---- content ---- */
244
+ #content { flex: 1; overflow-y: auto; padding: 28px 40px; background: var(--bg); }
245
+ .doc-content { width: 100%; }
246
+
247
+ .doc-content h1 { margin: 20px 0 14px; font-size: 24px; font-weight: 700; color: var(--text); border-bottom: 2px solid var(--accent); padding-bottom: 8px; }
248
+ .doc-content h2 { margin: 18px 0 10px; font-size: 19px; font-weight: 600; color: var(--text); }
249
+ .doc-content h3 { margin: 14px 0 8px; font-size: 16px; font-weight: 600; color: var(--text); }
250
+ .doc-content p { margin: 8px 0; line-height: 1.7; color: var(--text); font-size: 14px; }
251
+ .doc-content a { color: var(--accent); text-decoration: none; }
252
+ .doc-content a:hover { text-decoration: underline; }
253
+ .doc-content strong { font-weight: 600; }
254
+
255
+ /* table */
256
+ .doc-content table { border-collapse: collapse; width: 100%; margin: 14px 0; font-size: 13px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
257
+ .doc-content th, .doc-content td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
258
+ .doc-content th { background: var(--bg-soft); font-weight: 600; color: var(--text); white-space: nowrap; }
259
+ .doc-content td { color: var(--text); }
260
+ .doc-content tbody tr:hover { background: rgba(122,162,247,.04); }
261
+ .doc-content td code { white-space: nowrap; }
262
+
263
+ /* code */
264
+ .doc-content pre { background: var(--code-bg); color: var(--code-text); padding: 14px 18px; border-radius: 8px; overflow-x: auto; margin: 14px 0; font-size: 13px; line-height: 1.5; box-shadow: 0 2px 8px rgba(0,0,0,.1); }
265
+ .doc-content pre code { font-family: "SF Mono", "Fira Code", Menlo, monospace; }
266
+ .doc-content code { font-family: "SF Mono", "Fira Code", Menlo, monospace; font-size: 12.5px; background: var(--bg-soft); padding: 1px 5px; border-radius: 3px; color: #db4b4b; }
267
+ .doc-content pre code { background: none; color: inherit; padding: 0; border-radius: 0; font-size: inherit; }
268
+
269
+ /* list */
270
+ .doc-content ul { margin: 8px 0 8px 22px; }
271
+ .doc-content li { margin: 5px 0; color: var(--text); font-size: 14px; line-height: 1.6; }
272
+
273
+ /* blockquote */
274
+ .doc-content blockquote { border-left: 3px solid var(--accent); padding: 6px 14px; margin: 10px 0; background: var(--bg-soft); border-radius: 0 4px 4px 0; color: var(--text-soft); font-size: 13px; }
275
+ </style>
276
+ </head>
277
+ <body>
278
+ <div id="header">
279
+ <span class="logo">OPM</span>
280
+ <span class="title">Once Package Manager<span class="sub">— unified CLI docs</span></span>
281
+ </div>
282
+ <div id="body-row">
283
+ <div id="sidebar">
284
+ ${menuItems.join('\n')}
285
+ </div>
286
+ <div id="content">
287
+ ${contentDivs.join('\n')}
288
+ </div>
289
+ </div>
290
+ <script>
291
+ function show(id) {
292
+ document.querySelectorAll('.doc-content').forEach(el => el.style.display = 'none');
293
+ document.getElementById(id).style.display = 'block';
294
+ document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
295
+ const link = document.querySelector('.menu-item[onclick*="' + id + '"]');
296
+ if (link) link.classList.add('active');
297
+ }
298
+ // 默认激活第一个
299
+ (function() {
300
+ var first = document.querySelector('.menu-item');
301
+ if (first) first.classList.add('active');
302
+ })();
303
+ </script>
304
+ </body>
305
+ </html>`
306
+
307
+ fs.writeFileSync(OUTPUT, html, 'utf8')
308
+ console.log(`Generated ${OUTPUT} (${docs.length} docs)`)
309
+
310
+ if (open) {
311
+ openInBrowser(OUTPUT)
312
+ }
313
+ }
314
+
315
+ /** 取路径最后一段(不含扩展名) */
316
+ function parts_last(relPath) {
317
+ const parts = relPath.split('/')
318
+ return parts[parts.length - 1]
319
+ }
320
+
321
+ /** 跨平台打开浏览器 */
322
+ function openInBrowser(url) {
323
+ const platform = os.platform()
324
+ try {
325
+ if (platform === 'darwin') {
326
+ execSync(`open "${url}"`)
327
+ } else if (platform === 'win32') {
328
+ execSync(`start "" "${url}"`, { shell: 'cmd.exe' })
329
+ } else {
330
+ execSync(`xdg-open "${url}"`)
331
+ }
332
+ } catch {
333
+ console.error(`Failed to open browser. HTML at: ${url}`)
334
+ }
335
+ }