@neuxnet/neux-cli 0.2.3
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 +438 -0
- package/README.zh-CN.md +554 -0
- package/assets/container/assets/index.css +1 -0
- package/assets/container/assets/index.js +137 -0
- package/assets/container/assets/pageFrame.css +1 -0
- package/assets/container/assets/pageFrame.js +56 -0
- package/assets/container/assets/service.js +6 -0
- package/assets/container/assets/vconsole.js +931 -0
- package/assets/container/favicon.ico +0 -0
- package/assets/container/images/icon-arrow.png +0 -0
- package/assets/container/images/mini-action-white.png +0 -0
- package/assets/container/images/mini-action.png +0 -0
- package/assets/container/images/mini-arrow-left-white.png +0 -0
- package/assets/container/images/mini-arrow-left.jpg +0 -0
- package/assets/container/images/mini-arrow-left.png +0 -0
- package/assets/container/images/mini-close-white.png +0 -0
- package/assets/container/images/mini-close.png +0 -0
- package/assets/container/images/more.png +0 -0
- package/assets/container/images/search.jpg +0 -0
- package/assets/container/index.html +1 -0
- package/assets/container/pageFrame.html +1 -0
- package/assets/init/tabbar/home-active.png +0 -0
- package/assets/init/tabbar/home.png +0 -0
- package/assets/init/tabbar/list-active.png +0 -0
- package/assets/init/tabbar/list.png +0 -0
- package/assets/init/types/neux-api.d.ts +1402 -0
- package/package.json +68 -0
- package/scripts/generate-init-types.js +210 -0
- package/scripts/sync-compiler.js +22 -0
- package/scripts/sync-web-container.js +34 -0
- package/src/bin/cli.js +646 -0
- package/src/core/brand.js +11 -0
- package/src/core/compiler.js +305 -0
- package/src/core/defaults.js +12 -0
- package/src/core/dev.js +15 -0
- package/src/core/errors.js +17 -0
- package/src/core/fs.js +66 -0
- package/src/core/i18n.js +37 -0
- package/src/core/init.js +866 -0
- package/src/core/lifecycle.js +32 -0
- package/src/core/manifest.js +72 -0
- package/src/core/pack.js +156 -0
- package/src/core/package-info.js +27 -0
- package/src/core/preview-server.js +123 -0
- package/src/core/project.js +101 -0
- package/src/core/prompts.js +71 -0
- package/src/core/proxy-security.js +141 -0
- package/src/core/proxy.js +156 -0
- package/src/core/qr.js +41 -0
- package/src/core/terminal-qr.js +72 -0
- package/src/core/update.js +278 -0
- package/src/core/watch.js +113 -0
- package/src/core/web.js +649 -0
- package/src/core/zip.js +120 -0
- package/src/index.js +20 -0
- package/src/providers/service-client.js +149 -0
- package/src/providers/service-config.js +264 -0
- package/src/providers/service.js +146 -0
- package/src/providers/upload.js +112 -0
- package/vendor/dimina-compiler/bin/index.cjs +265 -0
- package/vendor/dimina-compiler/bin/index.js +263 -0
- package/vendor/dimina-compiler/compatibility-B-DoZtUX.cjs +395 -0
- package/vendor/dimina-compiler/compatibility-Cl3-DO6V.js +366 -0
- package/vendor/dimina-compiler/core/logic-compiler.cjs +378 -0
- package/vendor/dimina-compiler/core/logic-compiler.js +374 -0
- package/vendor/dimina-compiler/core/style-compiler.cjs +392 -0
- package/vendor/dimina-compiler/core/style-compiler.js +377 -0
- package/vendor/dimina-compiler/core/view-compiler.cjs +1601 -0
- package/vendor/dimina-compiler/core/view-compiler.js +1582 -0
- package/vendor/dimina-compiler/index.cjs +762 -0
- package/vendor/dimina-compiler/index.js +751 -0
- package/vendor/dimina-compiler/sourcemap-BgtIgqkC.cjs +1377 -0
- package/vendor/dimina-compiler/sourcemap-CKjhV9h7.js +1135 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import dns from 'node:dns'
|
|
2
|
+
import net from 'node:net'
|
|
3
|
+
|
|
4
|
+
const blockedAddresses = new net.BlockList()
|
|
5
|
+
|
|
6
|
+
for (const [address, prefix] of [
|
|
7
|
+
['0.0.0.0', 8],
|
|
8
|
+
['10.0.0.0', 8],
|
|
9
|
+
['100.64.0.0', 10],
|
|
10
|
+
['127.0.0.0', 8],
|
|
11
|
+
['169.254.0.0', 16],
|
|
12
|
+
['172.16.0.0', 12],
|
|
13
|
+
['192.0.0.0', 24],
|
|
14
|
+
['192.0.2.0', 24],
|
|
15
|
+
['192.168.0.0', 16],
|
|
16
|
+
['198.18.0.0', 15],
|
|
17
|
+
['198.51.100.0', 24],
|
|
18
|
+
['203.0.113.0', 24],
|
|
19
|
+
['224.0.0.0', 4],
|
|
20
|
+
['240.0.0.0', 4],
|
|
21
|
+
]) {
|
|
22
|
+
blockedAddresses.addSubnet(address, prefix, 'ipv4')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const [address, prefix] of [
|
|
26
|
+
['::', 128],
|
|
27
|
+
['::1', 128],
|
|
28
|
+
['64:ff9b:1::', 48],
|
|
29
|
+
['100::', 64],
|
|
30
|
+
['2001:db8::', 32],
|
|
31
|
+
['fc00::', 7],
|
|
32
|
+
['fe80::', 10],
|
|
33
|
+
['ff00::', 8],
|
|
34
|
+
]) {
|
|
35
|
+
blockedAddresses.addSubnet(address, prefix, 'ipv6')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BLOCKED_REQUEST_HEADERS = new Set([
|
|
39
|
+
'connection', 'content-length', 'forwarded', 'host', 'origin',
|
|
40
|
+
'proxy-authenticate', 'proxy-authorization', 'referer', 'te',
|
|
41
|
+
'trailer', 'transfer-encoding', 'upgrade', 'via',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
function unsafeTargetError(message) {
|
|
45
|
+
const error = new Error(message)
|
|
46
|
+
error.code = 'DIMINA_UNSAFE_TARGET'
|
|
47
|
+
return error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeHostname(hostname) {
|
|
51
|
+
const value = hostname.startsWith('[') && hostname.endsWith(']')
|
|
52
|
+
? hostname.slice(1, -1)
|
|
53
|
+
: hostname
|
|
54
|
+
return value.replace(/\.$/, '').toLowerCase()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isPublicAddress(address) {
|
|
58
|
+
if (address.toLowerCase().startsWith('::ffff:')) return false
|
|
59
|
+
const family = net.isIP(address)
|
|
60
|
+
return family !== 0 && !blockedAddresses.check(address, family === 4 ? 'ipv4' : 'ipv6')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertPublicAddresses(addresses) {
|
|
64
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
65
|
+
throw unsafeTargetError('Target hostname did not resolve')
|
|
66
|
+
}
|
|
67
|
+
for (const entry of addresses) {
|
|
68
|
+
if (!isPublicAddress(entry.address)) {
|
|
69
|
+
throw unsafeTargetError('Requests to private or reserved networks are not allowed')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function assertSafeTarget(rawUrl, lookup = dns.promises.lookup) {
|
|
75
|
+
if (typeof rawUrl !== 'string' || rawUrl.trim() === '') {
|
|
76
|
+
throw unsafeTargetError('URL is required')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let target
|
|
80
|
+
try {
|
|
81
|
+
target = new URL(rawUrl)
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw unsafeTargetError('Invalid URL')
|
|
85
|
+
}
|
|
86
|
+
if (!['http:', 'https:'].includes(target.protocol)) {
|
|
87
|
+
throw unsafeTargetError('Only HTTP and HTTPS URLs are allowed')
|
|
88
|
+
}
|
|
89
|
+
if (target.username || target.password) {
|
|
90
|
+
throw unsafeTargetError('Credentials in target URLs are not allowed')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hostname = normalizeHostname(target.hostname)
|
|
94
|
+
const family = net.isIP(hostname)
|
|
95
|
+
const addresses = family
|
|
96
|
+
? [{ address: hostname, family }]
|
|
97
|
+
: await lookup(hostname, { all: true, verbatim: true })
|
|
98
|
+
assertPublicAddresses(addresses)
|
|
99
|
+
return target
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createSafeLookup(lookup = dns.lookup) {
|
|
103
|
+
return (hostname, options, callback) => {
|
|
104
|
+
const normalizedOptions = typeof options === 'number' ? { family: options } : { ...(options ?? {}) }
|
|
105
|
+
lookup(hostname, { ...normalizedOptions, all: true, verbatim: true }, (error, addresses) => {
|
|
106
|
+
if (error) return callback(error)
|
|
107
|
+
try {
|
|
108
|
+
assertPublicAddresses(addresses)
|
|
109
|
+
}
|
|
110
|
+
catch (validationError) {
|
|
111
|
+
return callback(validationError)
|
|
112
|
+
}
|
|
113
|
+
if (normalizedOptions.all) return callback(null, addresses)
|
|
114
|
+
const [{ address, family }] = addresses
|
|
115
|
+
return callback(null, address, family)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isAllowedBrowserOrigin(origin, configuredOrigins = '') {
|
|
121
|
+
if (!origin) return true
|
|
122
|
+
const explicitOrigins = new Set(String(configuredOrigins).split(',').map(value => value.trim()).filter(Boolean))
|
|
123
|
+
if (explicitOrigins.has(origin)) return true
|
|
124
|
+
try {
|
|
125
|
+
const parsed = new URL(origin)
|
|
126
|
+
const hostname = normalizeHostname(parsed.hostname)
|
|
127
|
+
return ['http:', 'https:'].includes(parsed.protocol)
|
|
128
|
+
&& ['localhost', '127.0.0.1', '::1'].includes(hostname)
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function sanitizeRequestHeaders(headers) {
|
|
136
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) return {}
|
|
137
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => {
|
|
138
|
+
const normalizedName = name.toLowerCase()
|
|
139
|
+
return !BLOCKED_REQUEST_HEADERS.has(normalizedName) && !normalizedName.startsWith('proxy-')
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import { assertSafeTarget, createSafeLookup, isAllowedBrowserOrigin, sanitizeRequestHeaders } from './proxy-security.js'
|
|
4
|
+
|
|
5
|
+
const MAX_REQUEST_BODY_BYTES = 1024 * 1024
|
|
6
|
+
const MAX_RESPONSE_BODY_BYTES = 10 * 1024 * 1024
|
|
7
|
+
const ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
|
|
8
|
+
const ALLOWED_RESPONSE_TYPES = new Set(['json', 'text', 'arraybuffer'])
|
|
9
|
+
|
|
10
|
+
function writeJson(res, statusCode, body) {
|
|
11
|
+
res.writeHead(statusCode, {
|
|
12
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
13
|
+
'Cache-Control': 'no-store',
|
|
14
|
+
})
|
|
15
|
+
res.end(JSON.stringify(body))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setCorsHeaders(res, origin, allowedOrigins) {
|
|
19
|
+
if (!origin || !isAllowedBrowserOrigin(origin, allowedOrigins)) return false
|
|
20
|
+
if (origin) res.setHeader('Access-Control-Allow-Origin', origin)
|
|
21
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
|
22
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
23
|
+
res.setHeader('Vary', 'Origin')
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readBody(req) {
|
|
28
|
+
const chunks = []
|
|
29
|
+
let size = 0
|
|
30
|
+
for await (const chunk of req) {
|
|
31
|
+
size += chunk.length
|
|
32
|
+
if (size > MAX_REQUEST_BODY_BYTES) {
|
|
33
|
+
const error = new Error('Request body is too large')
|
|
34
|
+
error.code = 'DIMINA_PROXY_REQUEST_TOO_LARGE'
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
chunks.push(chunk)
|
|
38
|
+
}
|
|
39
|
+
if (chunks.length === 0) return {}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
const error = new Error('Request body must be valid JSON')
|
|
45
|
+
error.code = 'DIMINA_PROXY_INVALID_JSON'
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function requestTarget(target, options) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const transport = target.protocol === 'https:' ? https : http
|
|
53
|
+
const request = transport.request(target, {
|
|
54
|
+
method: options.method,
|
|
55
|
+
headers: options.headers,
|
|
56
|
+
agent: target.protocol === 'https:' ? options.httpsAgent : options.httpAgent,
|
|
57
|
+
timeout: options.timeout,
|
|
58
|
+
lookup: options.lookup,
|
|
59
|
+
}, response => {
|
|
60
|
+
const chunks = []
|
|
61
|
+
let size = 0
|
|
62
|
+
response.on('data', chunk => {
|
|
63
|
+
size += chunk.length
|
|
64
|
+
if (size > MAX_RESPONSE_BODY_BYTES) {
|
|
65
|
+
request.destroy(Object.assign(new Error('Response body is too large'), {
|
|
66
|
+
code: 'DIMINA_PROXY_RESPONSE_TOO_LARGE',
|
|
67
|
+
}))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
chunks.push(chunk)
|
|
71
|
+
})
|
|
72
|
+
response.on('end', () => resolve({
|
|
73
|
+
statusCode: response.statusCode || 502,
|
|
74
|
+
headers: response.headers,
|
|
75
|
+
body: Buffer.concat(chunks),
|
|
76
|
+
}))
|
|
77
|
+
})
|
|
78
|
+
request.on('timeout', () => request.destroy(Object.assign(new Error('Proxy request timed out'), {
|
|
79
|
+
code: 'DIMINA_PROXY_TIMEOUT',
|
|
80
|
+
})))
|
|
81
|
+
request.on('error', reject)
|
|
82
|
+
if (options.body) request.write(options.body)
|
|
83
|
+
request.end()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseTimeout(value) {
|
|
88
|
+
const parsed = Number.parseInt(value, 10)
|
|
89
|
+
return Number.isFinite(parsed) ? Math.min(Math.max(parsed, 1), 30000) : 30000
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createProxyHandler({
|
|
93
|
+
allowedOrigins = process.env.DIMINA_PROXY_ALLOWED_ORIGINS,
|
|
94
|
+
lookup = createSafeLookup(),
|
|
95
|
+
assertTarget = assertSafeTarget,
|
|
96
|
+
httpAgent = new http.Agent({ lookup }),
|
|
97
|
+
httpsAgent = new https.Agent({ lookup }),
|
|
98
|
+
} = {}) {
|
|
99
|
+
return async function handleProxy(req, res) {
|
|
100
|
+
const origin = req.headers.origin
|
|
101
|
+
if (!setCorsHeaders(res, origin, allowedOrigins)) {
|
|
102
|
+
writeJson(res, 403, { error: 'Browser origin is not allowed' })
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (req.method === 'OPTIONS') {
|
|
106
|
+
res.writeHead(204)
|
|
107
|
+
res.end()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (req.method !== 'POST') {
|
|
111
|
+
writeJson(res, 405, { error: 'Proxy only accepts POST requests' })
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const { url, data, header = {}, timeout = 30000, method = 'GET', responseType = 'json' } = await readBody(req)
|
|
117
|
+
const normalizedMethod = String(method).toUpperCase()
|
|
118
|
+
if (!ALLOWED_METHODS.has(normalizedMethod)) return writeJson(res, 400, { error: 'Invalid HTTP method' })
|
|
119
|
+
if (!ALLOWED_RESPONSE_TYPES.has(responseType)) return writeJson(res, 400, { error: 'Invalid response type' })
|
|
120
|
+
|
|
121
|
+
const target = await assertTarget(url)
|
|
122
|
+
if (normalizedMethod === 'GET' && data && typeof data === 'object') {
|
|
123
|
+
for (const [key, value] of Object.entries(data)) {
|
|
124
|
+
if (value !== undefined && value !== null) target.searchParams.set(key, String(value))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const body = normalizedMethod === 'GET' ? null : JSON.stringify(data ?? {})
|
|
128
|
+
const response = await requestTarget(target, {
|
|
129
|
+
method: normalizedMethod,
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
133
|
+
...sanitizeRequestHeaders(header),
|
|
134
|
+
},
|
|
135
|
+
body,
|
|
136
|
+
timeout: parseTimeout(timeout),
|
|
137
|
+
lookup,
|
|
138
|
+
httpAgent,
|
|
139
|
+
httpsAgent,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const contentType = response.headers['content-type']
|
|
143
|
+
|| (responseType === 'json' ? 'application/json' : responseType === 'text' ? 'text/plain; charset=utf-8' : 'application/octet-stream')
|
|
144
|
+
res.writeHead(response.statusCode, { 'Content-Type': contentType, 'Cache-Control': 'no-store' })
|
|
145
|
+
res.end(response.body)
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const statusCode = error.code === 'DIMINA_UNSAFE_TARGET' ? 403
|
|
149
|
+
: error.code === 'DIMINA_PROXY_INVALID_JSON' || error.code === 'DIMINA_PROXY_REQUEST_TOO_LARGE' ? 400
|
|
150
|
+
: error.code === 'DIMINA_PROXY_RESPONSE_TOO_LARGE' ? 502
|
|
151
|
+
: error.code === 'DIMINA_PROXY_TIMEOUT' ? 504
|
|
152
|
+
: 502
|
|
153
|
+
writeJson(res, statusCode, { error: error.message || 'Proxy request failed', status: statusCode })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/core/qr.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function createPreviewLaunchParams(options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
pagePath: options.pagePath || options.page_path || options.miniappPath || options.miniapp_path,
|
|
4
|
+
query: options.query || options.miniappQuery || options.miniapp_query,
|
|
5
|
+
scene: options.scene || options.miniappScene || options.miniapp_scene,
|
|
6
|
+
launchFrom: options.launchFrom || options.launch_from || options.miniappLaunchFrom || options.miniapp_launch_from,
|
|
7
|
+
location: options.location || options.miniappLocation || options.miniapp_location,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createDebugPreviewDeepLink({ appId, name, download, version, ...launchOptions }) {
|
|
12
|
+
const launchParams = createPreviewLaunchParams(launchOptions)
|
|
13
|
+
const params = new URLSearchParams({
|
|
14
|
+
appId: String(appId || ''),
|
|
15
|
+
name: String(name || ''),
|
|
16
|
+
download: String(download || ''),
|
|
17
|
+
appVerType: '1',
|
|
18
|
+
version: String(version || ''),
|
|
19
|
+
})
|
|
20
|
+
for (const [key, value] of Object.entries(launchParams)) {
|
|
21
|
+
if (value !== undefined && value !== null && value !== '') params.set(key, String(value))
|
|
22
|
+
}
|
|
23
|
+
return `neuminiapp://miniapp/?${params.toString()}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createLocalPreviewDeepLink({ appId, download, version, ...launchOptions }) {
|
|
27
|
+
return createDebugPreviewDeepLink({
|
|
28
|
+
appId,
|
|
29
|
+
name: appId,
|
|
30
|
+
download,
|
|
31
|
+
version,
|
|
32
|
+
...launchOptions,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createQrPayloadResult(payload) {
|
|
37
|
+
return {
|
|
38
|
+
qrPayload: payload,
|
|
39
|
+
deepLink: payload,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import QRCode from 'qrcode'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_QR_OPTIONS = Object.freeze({
|
|
4
|
+
errorCorrectionLevel: 'M',
|
|
5
|
+
margin: 4,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PNG_OPTIONS = Object.freeze({
|
|
9
|
+
type: 'png',
|
|
10
|
+
...DEFAULT_QR_OPTIONS,
|
|
11
|
+
scale: 8,
|
|
12
|
+
color: {
|
|
13
|
+
dark: '#000000ff',
|
|
14
|
+
light: '#ffffffff',
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const ANSI_BLACK_FOREGROUND = 30
|
|
19
|
+
const ANSI_WHITE_FOREGROUND = 97
|
|
20
|
+
const ANSI_BLACK_BACKGROUND = 40
|
|
21
|
+
const ANSI_WHITE_BACKGROUND = 107
|
|
22
|
+
const ANSI_RESET = '\u001B[0m'
|
|
23
|
+
const UPPER_HALF_BLOCK = '\u2580'
|
|
24
|
+
|
|
25
|
+
function terminalCellStyle(topDark, bottomDark) {
|
|
26
|
+
const foreground = topDark ? ANSI_BLACK_FOREGROUND : ANSI_WHITE_FOREGROUND
|
|
27
|
+
const background = bottomDark ? ANSI_BLACK_BACKGROUND : ANSI_WHITE_BACKGROUND
|
|
28
|
+
return `\u001B[${foreground};${background}m`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderTerminalQr(payload, options = {}) {
|
|
32
|
+
const margin = Number.isInteger(options.margin) && options.margin >= 0
|
|
33
|
+
? options.margin
|
|
34
|
+
: DEFAULT_QR_OPTIONS.margin
|
|
35
|
+
const qr = QRCode.create(String(payload || ''), {
|
|
36
|
+
errorCorrectionLevel: options.errorCorrectionLevel || DEFAULT_QR_OPTIONS.errorCorrectionLevel,
|
|
37
|
+
})
|
|
38
|
+
const lines = []
|
|
39
|
+
|
|
40
|
+
const isDark = (row, column) => row >= 0
|
|
41
|
+
&& row < qr.modules.size
|
|
42
|
+
&& column >= 0
|
|
43
|
+
&& column < qr.modules.size
|
|
44
|
+
&& qr.modules.get(row, column) === 1
|
|
45
|
+
|
|
46
|
+
for (let row = -margin; row < qr.modules.size + margin; row += 2) {
|
|
47
|
+
let activeStyle = ''
|
|
48
|
+
let line = ''
|
|
49
|
+
for (let column = -margin; column < qr.modules.size + margin; column += 1) {
|
|
50
|
+
const style = terminalCellStyle(isDark(row, column), isDark(row + 1, column))
|
|
51
|
+
if (style !== activeStyle) {
|
|
52
|
+
line += style
|
|
53
|
+
activeStyle = style
|
|
54
|
+
}
|
|
55
|
+
line += UPPER_HALF_BLOCK
|
|
56
|
+
}
|
|
57
|
+
lines.push(`${line}${ANSI_RESET}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `${lines.join('\n')}\n`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function renderQrPng(payload, options = {}) {
|
|
64
|
+
return QRCode.toBuffer(String(payload || ''), {
|
|
65
|
+
...DEFAULT_PNG_OPTIONS,
|
|
66
|
+
...options,
|
|
67
|
+
color: {
|
|
68
|
+
...DEFAULT_PNG_OPTIONS.color,
|
|
69
|
+
...options.color,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { DiminaCliError } from './errors.js'
|
|
5
|
+
import { brand } from './brand.js'
|
|
6
|
+
import { getCliPackageInfo, getCliVersion } from './package-info.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
|
|
9
|
+
const AUTO_UPDATE_VALUES = new Set(['1', 'true', 'yes', 'on'])
|
|
10
|
+
|
|
11
|
+
export async function checkCliUpdate(options = {}) {
|
|
12
|
+
const result = await createCliUpdateCheck(options)
|
|
13
|
+
return toPublicCheckResult(result)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function updateCli(options = {}) {
|
|
17
|
+
const check = await createCliUpdateCheck(options)
|
|
18
|
+
const publicCheck = toPublicCheckResult(check)
|
|
19
|
+
if (options.check || options.dryRun || !check.updateAvailable) {
|
|
20
|
+
return {
|
|
21
|
+
...publicCheck,
|
|
22
|
+
installed: false,
|
|
23
|
+
reason: check.updateAvailable ? 'check-only' : 'already-current',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const target = `${check.packageName}@${check.latestVersion}`
|
|
28
|
+
const npmArgs = ['install', '-g', target]
|
|
29
|
+
if (shouldPassRegistryToInstall(check.registrySource)) {
|
|
30
|
+
npmArgs.push('--registry', check.registry)
|
|
31
|
+
}
|
|
32
|
+
const npmCommand = resolveNpmCommand(options)
|
|
33
|
+
const install = options.install || defaultInstall
|
|
34
|
+
const installResult = await install(npmCommand, npmArgs, options)
|
|
35
|
+
if (installResult.status !== 0) {
|
|
36
|
+
throw new DiminaCliError('NEUX_CLI_UPDATE_FAILED', `Failed to update ${check.packageName} to ${check.latestVersion}`, {
|
|
37
|
+
packageName: check.packageName,
|
|
38
|
+
version: check.latestVersion,
|
|
39
|
+
npmCommand,
|
|
40
|
+
npmArgs,
|
|
41
|
+
status: installResult.status,
|
|
42
|
+
stderr: installResult.stderr,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...publicCheck,
|
|
48
|
+
installed: true,
|
|
49
|
+
target,
|
|
50
|
+
npmCommand,
|
|
51
|
+
npmArgs,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function createCliUpdateCheck(options = {}) {
|
|
56
|
+
const packageName = options.packageName || brand.packageName
|
|
57
|
+
const currentVersion = options.currentVersion || await getCliVersion()
|
|
58
|
+
const { registry, registrySource } = await resolveRegistry(packageName, options)
|
|
59
|
+
const latest = await fetchLatestPackageInfo(packageName, registry, options)
|
|
60
|
+
const latestVersion = latest.version
|
|
61
|
+
const updateAvailable = compareVersions(latestVersion, currentVersion) > 0
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
command: 'update',
|
|
66
|
+
packageName,
|
|
67
|
+
registry,
|
|
68
|
+
registrySource,
|
|
69
|
+
currentVersion,
|
|
70
|
+
latestVersion,
|
|
71
|
+
updateAvailable,
|
|
72
|
+
latest,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function maybeAutoUpdateCli(options = {}) {
|
|
77
|
+
if (!isAutoUpdateEnabled(options)) return null
|
|
78
|
+
return await updateCli({
|
|
79
|
+
...options,
|
|
80
|
+
auto: true,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isAutoUpdateEnabled(options = {}) {
|
|
85
|
+
if (options.autoUpdate === false || options.no_auto_update) return false
|
|
86
|
+
if (options.autoUpdate === true || options.auto_update === true) return true
|
|
87
|
+
return AUTO_UPDATE_VALUES.has(String(process.env.NEUX_CLI_AUTO_UPDATE || '').trim().toLowerCase())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeRegistry(value) {
|
|
91
|
+
return String(value || DEFAULT_REGISTRY).trim().replace(/\/+$/, '')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { getCliPackageInfo, getCliVersion }
|
|
95
|
+
|
|
96
|
+
function resolveNpmCommand(options = {}) {
|
|
97
|
+
return options.npmCommand || process.env.NEUX_CLI_NPM || 'npm'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function resolveRegistry(packageName, options = {}) {
|
|
101
|
+
if (options.registry) {
|
|
102
|
+
return {
|
|
103
|
+
registry: normalizeRegistry(options.registry),
|
|
104
|
+
registrySource: 'explicit',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (process.env.NEUX_CLI_REGISTRY) {
|
|
108
|
+
return {
|
|
109
|
+
registry: normalizeRegistry(process.env.NEUX_CLI_REGISTRY),
|
|
110
|
+
registrySource: 'env',
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const configured = await resolveRegistryFromNpmConfig(packageName, options)
|
|
115
|
+
if (configured) return configured
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
registry: DEFAULT_REGISTRY,
|
|
119
|
+
registrySource: 'default',
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function resolveRegistryFromNpmConfig(packageName, options = {}) {
|
|
124
|
+
const getNpmConfig = options.getNpmConfig || getNpmConfigValue
|
|
125
|
+
const scope = packageName.startsWith('@') ? packageName.split('/')[0] : ''
|
|
126
|
+
if (scope) {
|
|
127
|
+
const scopeRegistry = await getNpmConfig(`${scope}:registry`, options)
|
|
128
|
+
if (isConfiguredRegistry(scopeRegistry)) {
|
|
129
|
+
return {
|
|
130
|
+
registry: normalizeRegistry(scopeRegistry),
|
|
131
|
+
registrySource: 'npm-scope',
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const registry = await getNpmConfig('registry', options)
|
|
137
|
+
if (isConfiguredRegistry(registry)) {
|
|
138
|
+
return {
|
|
139
|
+
registry: normalizeRegistry(registry),
|
|
140
|
+
registrySource: 'npm',
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getNpmConfigValue(key, options = {}) {
|
|
148
|
+
const result = spawnSync(resolveNpmCommand(options), ['config', 'get', key], {
|
|
149
|
+
cwd: options.cwd || process.cwd(),
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
152
|
+
})
|
|
153
|
+
if (result.error || result.status !== 0) return null
|
|
154
|
+
return result.stdout.trim()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isConfiguredRegistry(value) {
|
|
158
|
+
if (!value) return false
|
|
159
|
+
const normalized = String(value).trim()
|
|
160
|
+
return normalized !== '' && normalized !== 'undefined' && normalized !== 'null'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function shouldPassRegistryToInstall(registrySource) {
|
|
164
|
+
return registrySource !== 'npm-scope'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function toPublicCheckResult(result) {
|
|
168
|
+
const { registrySource, ...publicResult } = result
|
|
169
|
+
return publicResult
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function fetchLatestPackageInfo(packageName, registry, options = {}) {
|
|
173
|
+
if (options.fetchLatest) return await options.fetchLatest(packageName, registry)
|
|
174
|
+
const encoded = packageName.startsWith('@')
|
|
175
|
+
? `@${encodeURIComponent(packageName.slice(1))}`
|
|
176
|
+
: encodeURIComponent(packageName)
|
|
177
|
+
const latestUrl = `${registry}/${encoded}/latest`
|
|
178
|
+
try {
|
|
179
|
+
const payload = await fetchJson(latestUrl, options)
|
|
180
|
+
if (!payload?.version) {
|
|
181
|
+
throw new DiminaCliError('NEUX_CLI_UPDATE_METADATA_INVALID', `Registry latest metadata is missing version for ${packageName}`, {
|
|
182
|
+
packageName,
|
|
183
|
+
registry,
|
|
184
|
+
url: latestUrl,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
return payload
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (!shouldFallbackToPackageDocument(error)) throw error
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const packageUrl = `${registry}/${encoded}`
|
|
194
|
+
const payload = await fetchJson(packageUrl, options)
|
|
195
|
+
const latestVersion = payload?.['dist-tags']?.latest
|
|
196
|
+
const latest = latestVersion && payload?.versions?.[latestVersion]
|
|
197
|
+
if (!latestVersion || !latest?.version) {
|
|
198
|
+
throw new DiminaCliError('NEUX_CLI_UPDATE_METADATA_INVALID', `Registry package metadata is missing dist-tags.latest for ${packageName}`, {
|
|
199
|
+
packageName,
|
|
200
|
+
registry,
|
|
201
|
+
url: packageUrl,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
return latest
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function shouldFallbackToPackageDocument(error) {
|
|
208
|
+
return error instanceof DiminaCliError
|
|
209
|
+
&& error.code === 'NEUX_CLI_UPDATE_METADATA_FAILED'
|
|
210
|
+
&& (error.details?.statusCode === 400 || error.details?.statusCode === 404)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function fetchJson(url, options = {}) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const client = url.startsWith('http://') ? http : https
|
|
216
|
+
const request = client.get(url, {
|
|
217
|
+
timeout: Number.parseInt(options.timeout || process.env.NEUX_CLI_UPDATE_TIMEOUT || 10000, 10),
|
|
218
|
+
headers: {
|
|
219
|
+
'Accept': 'application/json',
|
|
220
|
+
'User-Agent': `${brand.packageName}`,
|
|
221
|
+
},
|
|
222
|
+
}, (response) => {
|
|
223
|
+
const chunks = []
|
|
224
|
+
response.on('data', chunk => chunks.push(chunk))
|
|
225
|
+
response.on('end', () => {
|
|
226
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
227
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
228
|
+
reject(new DiminaCliError('NEUX_CLI_UPDATE_METADATA_FAILED', `Failed to fetch CLI update metadata: HTTP ${response.statusCode}`, {
|
|
229
|
+
url,
|
|
230
|
+
statusCode: response.statusCode,
|
|
231
|
+
body,
|
|
232
|
+
}))
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
resolve(JSON.parse(body))
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
reject(new DiminaCliError('NEUX_CLI_UPDATE_METADATA_INVALID', `Failed to parse CLI update metadata: ${error.message}`, {
|
|
240
|
+
url,
|
|
241
|
+
}))
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
request.on('timeout', () => request.destroy(new Error('update metadata request timed out')))
|
|
246
|
+
request.on('error', reject)
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function defaultInstall(command, args, options = {}) {
|
|
251
|
+
const result = spawnSync(command, args, {
|
|
252
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
253
|
+
encoding: 'utf8',
|
|
254
|
+
})
|
|
255
|
+
return {
|
|
256
|
+
status: result.status,
|
|
257
|
+
stderr: result.stderr,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compareVersions(left, right) {
|
|
262
|
+
const a = parseVersion(left)
|
|
263
|
+
const b = parseVersion(right)
|
|
264
|
+
for (let i = 0; i < 3; i++) {
|
|
265
|
+
if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i] ? 1 : -1
|
|
266
|
+
}
|
|
267
|
+
if (a.pre === b.pre) return 0
|
|
268
|
+
if (!a.pre) return 1
|
|
269
|
+
if (!b.pre) return -1
|
|
270
|
+
return a.pre > b.pre ? 1 : a.pre < b.pre ? -1 : 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseVersion(value) {
|
|
274
|
+
const [coreValue, pre = ''] = String(value || '0.0.0').split('-', 2)
|
|
275
|
+
const core = coreValue.split('.').map(part => Number.parseInt(part, 10) || 0)
|
|
276
|
+
while (core.length < 3) core.push(0)
|
|
277
|
+
return { core: core.slice(0, 3), pre }
|
|
278
|
+
}
|