@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.
Files changed (73) hide show
  1. package/README.md +438 -0
  2. package/README.zh-CN.md +554 -0
  3. package/assets/container/assets/index.css +1 -0
  4. package/assets/container/assets/index.js +137 -0
  5. package/assets/container/assets/pageFrame.css +1 -0
  6. package/assets/container/assets/pageFrame.js +56 -0
  7. package/assets/container/assets/service.js +6 -0
  8. package/assets/container/assets/vconsole.js +931 -0
  9. package/assets/container/favicon.ico +0 -0
  10. package/assets/container/images/icon-arrow.png +0 -0
  11. package/assets/container/images/mini-action-white.png +0 -0
  12. package/assets/container/images/mini-action.png +0 -0
  13. package/assets/container/images/mini-arrow-left-white.png +0 -0
  14. package/assets/container/images/mini-arrow-left.jpg +0 -0
  15. package/assets/container/images/mini-arrow-left.png +0 -0
  16. package/assets/container/images/mini-close-white.png +0 -0
  17. package/assets/container/images/mini-close.png +0 -0
  18. package/assets/container/images/more.png +0 -0
  19. package/assets/container/images/search.jpg +0 -0
  20. package/assets/container/index.html +1 -0
  21. package/assets/container/pageFrame.html +1 -0
  22. package/assets/init/tabbar/home-active.png +0 -0
  23. package/assets/init/tabbar/home.png +0 -0
  24. package/assets/init/tabbar/list-active.png +0 -0
  25. package/assets/init/tabbar/list.png +0 -0
  26. package/assets/init/types/neux-api.d.ts +1402 -0
  27. package/package.json +68 -0
  28. package/scripts/generate-init-types.js +210 -0
  29. package/scripts/sync-compiler.js +22 -0
  30. package/scripts/sync-web-container.js +34 -0
  31. package/src/bin/cli.js +646 -0
  32. package/src/core/brand.js +11 -0
  33. package/src/core/compiler.js +305 -0
  34. package/src/core/defaults.js +12 -0
  35. package/src/core/dev.js +15 -0
  36. package/src/core/errors.js +17 -0
  37. package/src/core/fs.js +66 -0
  38. package/src/core/i18n.js +37 -0
  39. package/src/core/init.js +866 -0
  40. package/src/core/lifecycle.js +32 -0
  41. package/src/core/manifest.js +72 -0
  42. package/src/core/pack.js +156 -0
  43. package/src/core/package-info.js +27 -0
  44. package/src/core/preview-server.js +123 -0
  45. package/src/core/project.js +101 -0
  46. package/src/core/prompts.js +71 -0
  47. package/src/core/proxy-security.js +141 -0
  48. package/src/core/proxy.js +156 -0
  49. package/src/core/qr.js +41 -0
  50. package/src/core/terminal-qr.js +72 -0
  51. package/src/core/update.js +278 -0
  52. package/src/core/watch.js +113 -0
  53. package/src/core/web.js +649 -0
  54. package/src/core/zip.js +120 -0
  55. package/src/index.js +20 -0
  56. package/src/providers/service-client.js +149 -0
  57. package/src/providers/service-config.js +264 -0
  58. package/src/providers/service.js +146 -0
  59. package/src/providers/upload.js +112 -0
  60. package/vendor/dimina-compiler/bin/index.cjs +265 -0
  61. package/vendor/dimina-compiler/bin/index.js +263 -0
  62. package/vendor/dimina-compiler/compatibility-B-DoZtUX.cjs +395 -0
  63. package/vendor/dimina-compiler/compatibility-Cl3-DO6V.js +366 -0
  64. package/vendor/dimina-compiler/core/logic-compiler.cjs +378 -0
  65. package/vendor/dimina-compiler/core/logic-compiler.js +374 -0
  66. package/vendor/dimina-compiler/core/style-compiler.cjs +392 -0
  67. package/vendor/dimina-compiler/core/style-compiler.js +377 -0
  68. package/vendor/dimina-compiler/core/view-compiler.cjs +1601 -0
  69. package/vendor/dimina-compiler/core/view-compiler.js +1582 -0
  70. package/vendor/dimina-compiler/index.cjs +762 -0
  71. package/vendor/dimina-compiler/index.js +751 -0
  72. package/vendor/dimina-compiler/sourcemap-BgtIgqkC.cjs +1377 -0
  73. 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
+ }