@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.
- package/README.md +381 -0
- package/bin/index.js +9 -0
- package/docs/managers/apt.md +45 -0
- package/docs/managers/brew.md +92 -0
- package/docs/managers/bun.md +50 -0
- package/docs/managers/cft.md +57 -0
- package/docs/managers/composer.md +96 -0
- package/docs/managers/config.md +32 -0
- package/docs/managers/dnf.md +46 -0
- package/docs/managers/node.md +49 -0
- package/docs/managers/npm.md +100 -0
- package/docs/managers/nvm.md +34 -0
- package/docs/managers/php.md +48 -0
- package/docs/managers/phpenv.md +34 -0
- package/docs/managers/pip.md +98 -0
- package/docs/managers/python.md +53 -0
- package/docs/managers/uv.md +27 -0
- package/docs/opm.html +570 -0
- package/docs/packages/opencode.md +41 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +7 -0
- package/src/binaries/base.js +508 -0
- package/src/binaries/cft/index.js +35 -0
- package/src/binaries/depchecker.js +172 -0
- package/src/binaries/exefinder.js +110 -0
- package/src/binaries/platform.js +17 -0
- package/src/cli/doc.js +335 -0
- package/src/cli/groups/config.js +78 -0
- package/src/cli/groups/ensure.js +96 -0
- package/src/cli/groups/homebrew.js +73 -0
- package/src/cli/groups/nvm.js +73 -0
- package/src/cli/groups/package.js +514 -0
- package/src/cli/groups/phpenv.js +89 -0
- package/src/cli/groups/uv.js +73 -0
- package/src/cli/groups/version-manager.js +186 -0
- package/src/cli/helpers/args.js +49 -0
- package/src/cli/index.js +129 -0
- package/src/config.js +133 -0
- package/src/config.json5 +158 -0
- package/src/formatter.js +13 -0
- package/src/harnesses/base.js +205 -0
- package/src/harnesses/opencode/clear.js +239 -0
- package/src/harnesses/opencode/db.js +39 -0
- package/src/harnesses/opencode/index.js +46 -0
- package/src/harnesses/opencode/sqlite.js +200 -0
- package/src/index.js +46 -0
- package/src/managers/apt.js +377 -0
- package/src/managers/base.js +167 -0
- package/src/managers/brew.js +374 -0
- package/src/managers/bun.js +342 -0
- package/src/managers/composer.js +280 -0
- package/src/managers/dnf.js +370 -0
- package/src/managers/http.js +432 -0
- package/src/managers/node.js +150 -0
- package/src/managers/npm.js +473 -0
- package/src/managers/php.js +157 -0
- package/src/managers/pip.js +555 -0
- package/src/managers/python.js +180 -0
- package/src/managers/runtime.js +309 -0
- package/src/managers/winget.js +268 -0
- package/src/presets.js +24 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
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
|
+
}
|