@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
package/src/core/zip.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { listFiles } from './fs.js'
|
|
5
|
+
|
|
6
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, index) => {
|
|
7
|
+
let value = index
|
|
8
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
9
|
+
value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1)
|
|
10
|
+
}
|
|
11
|
+
return value >>> 0
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function crc32(buffer) {
|
|
15
|
+
let crc = 0xffffffff
|
|
16
|
+
for (const byte of buffer) {
|
|
17
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
|
18
|
+
}
|
|
19
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dosDateTime(date = new Date()) {
|
|
23
|
+
const year = Math.max(date.getFullYear(), 1980)
|
|
24
|
+
const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2)
|
|
25
|
+
const day = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()
|
|
26
|
+
return { time, day }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function localHeader(name, data, meta) {
|
|
30
|
+
const nameBuffer = Buffer.from(name)
|
|
31
|
+
const header = Buffer.alloc(30)
|
|
32
|
+
header.writeUInt32LE(0x04034b50, 0)
|
|
33
|
+
header.writeUInt16LE(20, 4)
|
|
34
|
+
header.writeUInt16LE(0x0800, 6)
|
|
35
|
+
header.writeUInt16LE(0, 8)
|
|
36
|
+
header.writeUInt16LE(meta.time, 10)
|
|
37
|
+
header.writeUInt16LE(meta.day, 12)
|
|
38
|
+
header.writeUInt32LE(meta.crc, 14)
|
|
39
|
+
header.writeUInt32LE(data.length, 18)
|
|
40
|
+
header.writeUInt32LE(data.length, 22)
|
|
41
|
+
header.writeUInt16LE(nameBuffer.length, 26)
|
|
42
|
+
return Buffer.concat([header, nameBuffer])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function centralHeader(name, data, meta, offset) {
|
|
46
|
+
const nameBuffer = Buffer.from(name)
|
|
47
|
+
const header = Buffer.alloc(46)
|
|
48
|
+
header.writeUInt32LE(0x02014b50, 0)
|
|
49
|
+
header.writeUInt16LE(20, 4)
|
|
50
|
+
header.writeUInt16LE(20, 6)
|
|
51
|
+
header.writeUInt16LE(0x0800, 8)
|
|
52
|
+
header.writeUInt16LE(0, 10)
|
|
53
|
+
header.writeUInt16LE(meta.time, 12)
|
|
54
|
+
header.writeUInt16LE(meta.day, 14)
|
|
55
|
+
header.writeUInt32LE(meta.crc, 16)
|
|
56
|
+
header.writeUInt32LE(data.length, 20)
|
|
57
|
+
header.writeUInt32LE(data.length, 24)
|
|
58
|
+
header.writeUInt16LE(nameBuffer.length, 28)
|
|
59
|
+
header.writeUInt32LE(offset, 42)
|
|
60
|
+
return Buffer.concat([header, nameBuffer])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function endOfCentralDirectory(entryCount, centralSize, centralOffset) {
|
|
64
|
+
const footer = Buffer.alloc(22)
|
|
65
|
+
footer.writeUInt32LE(0x06054b50, 0)
|
|
66
|
+
footer.writeUInt16LE(entryCount, 8)
|
|
67
|
+
footer.writeUInt16LE(entryCount, 10)
|
|
68
|
+
footer.writeUInt32LE(centralSize, 12)
|
|
69
|
+
footer.writeUInt32LE(centralOffset, 16)
|
|
70
|
+
return footer
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createZip(sourceDir, outputPath, extraEntries = []) {
|
|
74
|
+
const sourceRoot = path.resolve(sourceDir)
|
|
75
|
+
const diskFiles = await listFiles(sourceRoot)
|
|
76
|
+
const entries = []
|
|
77
|
+
|
|
78
|
+
for (const filePath of diskFiles) {
|
|
79
|
+
entries.push({
|
|
80
|
+
name: path.relative(sourceRoot, filePath).split(path.sep).join('/'),
|
|
81
|
+
data: await fs.readFile(filePath),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const entry of extraEntries) {
|
|
86
|
+
entries.push({
|
|
87
|
+
name: entry.name,
|
|
88
|
+
data: Buffer.isBuffer(entry.content) ? entry.content : Buffer.from(String(entry.content)),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const localParts = []
|
|
93
|
+
const centralParts = []
|
|
94
|
+
let offset = 0
|
|
95
|
+
const timestamp = dosDateTime()
|
|
96
|
+
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const meta = { ...timestamp, crc: crc32(entry.data) }
|
|
99
|
+
const local = localHeader(entry.name, entry.data, meta)
|
|
100
|
+
localParts.push(local, entry.data)
|
|
101
|
+
centralParts.push(centralHeader(entry.name, entry.data, meta, offset))
|
|
102
|
+
offset += local.length + entry.data.length
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const central = Buffer.concat(centralParts)
|
|
106
|
+
const archive = Buffer.concat([
|
|
107
|
+
...localParts,
|
|
108
|
+
central,
|
|
109
|
+
endOfCentralDirectory(entries.length, central.length, offset),
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
113
|
+
await fs.writeFile(outputPath, archive)
|
|
114
|
+
return {
|
|
115
|
+
path: outputPath,
|
|
116
|
+
size: archive.length,
|
|
117
|
+
sha256: crypto.createHash('sha256').update(archive).digest('hex'),
|
|
118
|
+
entryCount: entries.length,
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { buildProject, buildStyleOnlyProject, buildViewOnlyProject, buildLogicOnlyProject } from './core/compiler.js'
|
|
2
|
+
export { DEFAULT_SERVER_URL, initProject } from './core/init.js'
|
|
3
|
+
export { inspectProject } from './core/project.js'
|
|
4
|
+
export { packProject } from './core/pack.js'
|
|
5
|
+
export { startPreview } from './core/preview-server.js'
|
|
6
|
+
export { startDev } from './core/dev.js'
|
|
7
|
+
export { startWeb } from './core/web.js'
|
|
8
|
+
export { classifyHotUpdate, createHotUpdatePayloads } from './core/web.js'
|
|
9
|
+
export { checkCliUpdate, getCliPackageInfo, getCliVersion, isAutoUpdateEnabled, maybeAutoUpdateCli, updateCli } from './core/update.js'
|
|
10
|
+
export { createProjectWatcher } from './core/watch.js'
|
|
11
|
+
export { createLifecycleEvent, emitLifecycleEvent } from './core/lifecycle.js'
|
|
12
|
+
export { createAppConfig, createDeepLink, createManifest } from './core/manifest.js'
|
|
13
|
+
export { createLocalFileProvider, createUploadProvider, loadUploadProvider, runProviderCommand } from './providers/upload.js'
|
|
14
|
+
export { createServiceProvider } from './providers/service.js'
|
|
15
|
+
export { resolveServiceConfig, resolveServiceConfigDiagnostics, serviceEnvKeys, serviceKeyEnvKey } from './providers/service-config.js'
|
|
16
|
+
export { SERVICE_BASE_PATH, ServiceClient, buildCanonicalString, hmacSha256Base64, md5File, sha256Hex } from './providers/service-client.js'
|
|
17
|
+
export { createDebugPreviewDeepLink, createLocalPreviewDeepLink, createPreviewLaunchParams, createQrPayloadResult } from './core/qr.js'
|
|
18
|
+
export { saveCliKey } from './providers/service-config.js'
|
|
19
|
+
export { renderQrPng, renderTerminalQr } from './core/terminal-qr.js'
|
|
20
|
+
export { DiminaCliError } from './core/errors.js'
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { DiminaCliError } from '../core/errors.js'
|
|
5
|
+
|
|
6
|
+
export const SERVICE_BASE_PATH = '/miniappmgr/openapi/miniapp/cli'
|
|
7
|
+
|
|
8
|
+
export function buildCanonicalString({ method, requestPath, appId, timestamp, nonce, bodyHash = '' }) {
|
|
9
|
+
return [
|
|
10
|
+
String(method).toUpperCase(),
|
|
11
|
+
requestPath,
|
|
12
|
+
appId,
|
|
13
|
+
String(timestamp),
|
|
14
|
+
nonce,
|
|
15
|
+
bodyHash,
|
|
16
|
+
].join('\n')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sha256Hex(value) {
|
|
20
|
+
return crypto.createHash('sha256').update(value).digest('hex')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hmacSha256Base64(key, value) {
|
|
24
|
+
return crypto.createHmac('sha256', key).update(value).digest('base64')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function md5File(filePath) {
|
|
28
|
+
return crypto.createHash('md5').update(await fs.readFile(filePath)).digest('hex')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nonce() {
|
|
32
|
+
return crypto.randomBytes(16).toString('hex')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function serviceUrl(serverUrl, endpoint, query = {}) {
|
|
36
|
+
const requestPath = `${SERVICE_BASE_PATH}${endpoint}`
|
|
37
|
+
const url = new URL(requestPath, `${serverUrl.replace(/\/+$/, '')}/`)
|
|
38
|
+
for (const [key, value] of Object.entries(query)) {
|
|
39
|
+
if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value))
|
|
40
|
+
}
|
|
41
|
+
return { url, requestPath }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function signedHeaders(config, method, requestPath, bodyHash) {
|
|
45
|
+
const timestamp = Date.now()
|
|
46
|
+
const requestNonce = nonce()
|
|
47
|
+
const canonicalString = buildCanonicalString({
|
|
48
|
+
method,
|
|
49
|
+
requestPath,
|
|
50
|
+
appId: config.appId,
|
|
51
|
+
timestamp,
|
|
52
|
+
nonce: requestNonce,
|
|
53
|
+
bodyHash,
|
|
54
|
+
})
|
|
55
|
+
return {
|
|
56
|
+
canonicalString,
|
|
57
|
+
headers: {
|
|
58
|
+
'X-App-Id': config.appId,
|
|
59
|
+
'X-Timestamp': String(timestamp),
|
|
60
|
+
'X-Nonce': requestNonce,
|
|
61
|
+
'X-Signature': hmacSha256Base64(config.key, canonicalString),
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function unwrapCommonResponse(response) {
|
|
67
|
+
const text = await response.text()
|
|
68
|
+
let data = null
|
|
69
|
+
if (text) {
|
|
70
|
+
try {
|
|
71
|
+
data = JSON.parse(text)
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new DiminaCliError('NEUX_CLI_HTTP_ERROR', `Service request failed with HTTP ${response.status}`, {
|
|
76
|
+
httpStatus: response.status,
|
|
77
|
+
body: text.slice(0, 500),
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
throw new DiminaCliError('NEUX_CLI_RESPONSE_INVALID', 'Service response was not valid JSON.', {
|
|
81
|
+
httpStatus: response.status,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new DiminaCliError('NEUX_CLI_HTTP_ERROR', `Service request failed with HTTP ${response.status}`, {
|
|
88
|
+
httpStatus: response.status,
|
|
89
|
+
response: data,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const header = data?.responseHeader
|
|
94
|
+
if (header && header.status !== 200) {
|
|
95
|
+
throw new DiminaCliError('NEUX_CLI_SERVICE_ERROR', header.msg || 'Service returned an error.', {
|
|
96
|
+
serviceStatus: header.status,
|
|
97
|
+
serviceError: header.error,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return data && Object.hasOwn(data, 'response') ? data.response : data
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class ServiceClient {
|
|
104
|
+
constructor(config) {
|
|
105
|
+
this.config = config
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async postJson(endpoint, body = {}) {
|
|
109
|
+
const json = JSON.stringify(body)
|
|
110
|
+
const { url, requestPath } = serviceUrl(this.config.serverUrl, endpoint)
|
|
111
|
+
const { headers } = signedHeaders(this.config, 'POST', requestPath, sha256Hex(json))
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
...headers,
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: json,
|
|
119
|
+
})
|
|
120
|
+
return await unwrapCommonResponse(response)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async get(endpoint, query = {}) {
|
|
124
|
+
const { url, requestPath } = serviceUrl(this.config.serverUrl, endpoint, query)
|
|
125
|
+
const { headers } = signedHeaders(this.config, 'GET', requestPath, '')
|
|
126
|
+
const response = await fetch(url, { method: 'GET', headers })
|
|
127
|
+
return await unwrapCommonResponse(response)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async uploadPackage(filePath, fields = {}) {
|
|
131
|
+
const md5 = await md5File(filePath)
|
|
132
|
+
const { url, requestPath } = serviceUrl(this.config.serverUrl, '/upload')
|
|
133
|
+
const { headers } = signedHeaders(this.config, 'POST', requestPath, md5)
|
|
134
|
+
const bytes = await fs.readFile(filePath)
|
|
135
|
+
const form = new FormData()
|
|
136
|
+
form.set('file', new Blob([bytes]), path.basename(filePath))
|
|
137
|
+
form.set('appId', fields.appId || this.config.appId)
|
|
138
|
+
form.set('md5', md5)
|
|
139
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
140
|
+
if (value !== undefined && value !== null && value !== '') form.set(key, String(value))
|
|
141
|
+
}
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers,
|
|
145
|
+
body: form,
|
|
146
|
+
})
|
|
147
|
+
return await unwrapCommonResponse(response)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { DiminaCliError } from '../core/errors.js'
|
|
5
|
+
import { pathExists, readJson, resolvePath } from '../core/fs.js'
|
|
6
|
+
|
|
7
|
+
const ENV_KEYS = {
|
|
8
|
+
serverUrl: 'NEUX_CLI_SERVER_URL',
|
|
9
|
+
appId: 'NEUX_CLI_APP_ID',
|
|
10
|
+
profile: 'NEUX_CLI_PROFILE',
|
|
11
|
+
key: 'NEUX_CLI_KEY',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeEnvSuffix(value) {
|
|
15
|
+
return String(value || '')
|
|
16
|
+
.trim()
|
|
17
|
+
.toUpperCase()
|
|
18
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
19
|
+
.replace(/^_+|_+$/g, '')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function serviceKeyEnvKey(value) {
|
|
23
|
+
const suffix = normalizeEnvSuffix(value)
|
|
24
|
+
return suffix ? `${ENV_KEYS.key}_${suffix}` : ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function keyEnvCandidates({ appId, profile } = {}) {
|
|
28
|
+
return Array.from(new Set([
|
|
29
|
+
serviceKeyEnvKey(appId),
|
|
30
|
+
serviceKeyEnvKey(profile),
|
|
31
|
+
ENV_KEYS.key,
|
|
32
|
+
].filter(Boolean)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstValue(...values) {
|
|
36
|
+
return values.find(value => value !== undefined && value !== null && value !== '')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertNoKey(sourceName, value) {
|
|
40
|
+
if (!value || typeof value !== 'object') return
|
|
41
|
+
if (value.key || value.cliKey || value.neuxCliKey) {
|
|
42
|
+
throw new DiminaCliError('NEUX_CLI_KEY_NOT_ALLOWED', `${sourceName} must not contain a CLI key. Use --key or a NEUX_CLI_KEY_* environment variable.`, {
|
|
43
|
+
source: sourceName,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveKey({ options = {}, appId, profile, userKeys = {} } = {}) {
|
|
49
|
+
const envKeys = keyEnvCandidates({ appId, profile })
|
|
50
|
+
if (options.key) return { value: options.key, source: 'cli:--key', envKeys }
|
|
51
|
+
|
|
52
|
+
for (const envKey of envKeys) {
|
|
53
|
+
if (process.env[envKey]) return { value: process.env[envKey], source: `env:${envKey}`, envKeys }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const storedKey = userKeys[appId] || (profile ? userKeys[`profile:${profile}`] : '')
|
|
57
|
+
if (storedKey) return { value: storedKey, source: `user:${appId}`, envKeys }
|
|
58
|
+
|
|
59
|
+
return { value: undefined, source: null, envKeys }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function configEnvKeys(resolved = {}, keyEnvKeys = []) {
|
|
63
|
+
return {
|
|
64
|
+
serverUrl: ENV_KEYS.serverUrl,
|
|
65
|
+
appId: ENV_KEYS.appId,
|
|
66
|
+
profile: ENV_KEYS.profile,
|
|
67
|
+
key: keyEnvKeys,
|
|
68
|
+
keyForApp: serviceKeyEnvKey(resolved.appId),
|
|
69
|
+
keyForProfile: serviceKeyEnvKey(resolved.profile),
|
|
70
|
+
keyGeneric: ENV_KEYS.key,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function missingConfigSuggestions(missing, envKeys = {}) {
|
|
75
|
+
const suggestions = []
|
|
76
|
+
if (missing.includes('serverUrl')) {
|
|
77
|
+
suggestions.push(`export ${ENV_KEYS.serverUrl}=https://miniapp.example.com`)
|
|
78
|
+
suggestions.push('or pass --server-url https://miniapp.example.com')
|
|
79
|
+
}
|
|
80
|
+
if (missing.includes('appId')) {
|
|
81
|
+
suggestions.push(`export ${ENV_KEYS.appId}=<appId>`)
|
|
82
|
+
suggestions.push('or pass --app-id <appId>')
|
|
83
|
+
}
|
|
84
|
+
if (missing.includes('key')) {
|
|
85
|
+
if (envKeys.keyForApp) suggestions.push(`export ${envKeys.keyForApp}=<key>`)
|
|
86
|
+
if (envKeys.keyForProfile && envKeys.keyForProfile !== envKeys.keyForApp) {
|
|
87
|
+
suggestions.push(`export ${envKeys.keyForProfile}=<key>`)
|
|
88
|
+
}
|
|
89
|
+
suggestions.push(`export ${ENV_KEYS.key}=<key>`)
|
|
90
|
+
suggestions.push('or pass --key <key>')
|
|
91
|
+
}
|
|
92
|
+
return suggestions
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readOptionalJson(filePath) {
|
|
96
|
+
if (!(await pathExists(filePath))) return null
|
|
97
|
+
return await readJson(filePath)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readProjectServiceConfig(projectPath) {
|
|
101
|
+
const neuxConfigPath = path.join(projectPath, 'neux.config.json')
|
|
102
|
+
const neuxConfig = await readOptionalJson(neuxConfigPath)
|
|
103
|
+
if (neuxConfig) {
|
|
104
|
+
assertNoKey(neuxConfigPath, neuxConfig)
|
|
105
|
+
return {
|
|
106
|
+
appId: neuxConfig.appId || neuxConfig.appid,
|
|
107
|
+
profile: neuxConfig.profile,
|
|
108
|
+
serverUrl: neuxConfig.serverUrl,
|
|
109
|
+
source: neuxConfigPath,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const projectConfigPath = path.join(projectPath, 'project.config.json')
|
|
114
|
+
const projectPrivateConfigPath = path.join(projectPath, 'project.private.config.json')
|
|
115
|
+
const projectConfig = await readOptionalJson(projectConfigPath)
|
|
116
|
+
const projectPrivateConfig = await readOptionalJson(projectPrivateConfigPath)
|
|
117
|
+
const effectiveProjectConfig = mergeProjectConfig(projectConfig || {}, projectPrivateConfig || {})
|
|
118
|
+
const nested = {
|
|
119
|
+
...(effectiveProjectConfig.neux || {}),
|
|
120
|
+
...(effectiveProjectConfig.neuxCli || {}),
|
|
121
|
+
}
|
|
122
|
+
assertNoKey(projectConfigPath, projectConfig?.neuxCli || projectConfig?.neux || {})
|
|
123
|
+
assertNoKey(projectPrivateConfigPath, projectPrivateConfig?.neuxCli || projectPrivateConfig?.neux || {})
|
|
124
|
+
return {
|
|
125
|
+
appId: nested.appId || nested.appid || effectiveProjectConfig.appId || effectiveProjectConfig.appid,
|
|
126
|
+
profile: nested.profile,
|
|
127
|
+
serverUrl: nested.serverUrl || effectiveProjectConfig.serverUrl,
|
|
128
|
+
source: projectPrivateConfig ? projectPrivateConfigPath : projectConfig ? projectConfigPath : null,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mergeProjectConfig(projectConfig = {}, privateConfig = {}) {
|
|
133
|
+
return {
|
|
134
|
+
...projectConfig,
|
|
135
|
+
...privateConfig,
|
|
136
|
+
neux: {
|
|
137
|
+
...(projectConfig.neux || {}),
|
|
138
|
+
...(privateConfig.neux || {}),
|
|
139
|
+
},
|
|
140
|
+
neuxCli: {
|
|
141
|
+
...(projectConfig.neuxCli || {}),
|
|
142
|
+
...(privateConfig.neuxCli || {}),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readUserProfileConfig(options = {}) {
|
|
148
|
+
const configPath = resolvePath(
|
|
149
|
+
options.config || options.configPath || process.env.NEUX_CLI_CONFIG,
|
|
150
|
+
path.join(os.homedir(), '.neux', 'config.json'),
|
|
151
|
+
)
|
|
152
|
+
const config = await readOptionalJson(configPath)
|
|
153
|
+
if (!config) return { configPath, profiles: {}, keys: {}, raw: {} }
|
|
154
|
+
|
|
155
|
+
assertNoKey(configPath, config)
|
|
156
|
+
for (const [profileName, profile] of Object.entries(config.profiles || {})) {
|
|
157
|
+
assertNoKey(`${configPath} profile ${profileName}`, profile)
|
|
158
|
+
}
|
|
159
|
+
return { configPath, profiles: config.profiles || {}, keys: config.keys || {}, raw: config }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function saveCliKey({ appId, key, configPath } = {}) {
|
|
163
|
+
const normalizedAppId = String(appId || '').trim()
|
|
164
|
+
const normalizedKey = String(key || '').trim()
|
|
165
|
+
if (!normalizedAppId || !normalizedKey) {
|
|
166
|
+
throw new DiminaCliError('NEUX_CLI_KEY_INVALID', 'App ID and CLI Key are required.')
|
|
167
|
+
}
|
|
168
|
+
const userConfig = await readUserProfileConfig({ configPath })
|
|
169
|
+
const nextConfig = {
|
|
170
|
+
...userConfig.raw,
|
|
171
|
+
profiles: userConfig.profiles,
|
|
172
|
+
keys: {
|
|
173
|
+
...userConfig.keys,
|
|
174
|
+
[normalizedAppId]: normalizedKey,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
await fs.mkdir(path.dirname(userConfig.configPath), { recursive: true })
|
|
178
|
+
await fs.writeFile(userConfig.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, { mode: 0o600 })
|
|
179
|
+
await fs.chmod(userConfig.configPath, 0o600)
|
|
180
|
+
return { configPath: userConfig.configPath, appId: normalizedAppId }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeConfig(config) {
|
|
184
|
+
const normalized = { ...config }
|
|
185
|
+
if (normalized.serverUrl) normalized.serverUrl = String(normalized.serverUrl).replace(/\/+$/, '')
|
|
186
|
+
return normalized
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function resolveServiceConfig(options = {}, requirements = {}) {
|
|
190
|
+
const { resolved, envKeys } = await resolveServiceConfigSnapshot(options)
|
|
191
|
+
const missing = []
|
|
192
|
+
for (const field of requirements.required || ['serverUrl', 'appId', 'key']) {
|
|
193
|
+
if (!resolved[field]) missing.push(field)
|
|
194
|
+
}
|
|
195
|
+
if (missing.length > 0) {
|
|
196
|
+
throw new DiminaCliError('NEUX_CLI_CONFIG_MISSING', `Missing service configuration: ${missing.join(', ')}`, {
|
|
197
|
+
missing,
|
|
198
|
+
appId: resolved.appId,
|
|
199
|
+
profile: resolved.profile,
|
|
200
|
+
envKeys,
|
|
201
|
+
suggestions: missingConfigSuggestions(missing, envKeys),
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return resolved
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function resolveServiceConfigDiagnostics(options = {}) {
|
|
209
|
+
const { resolved, envKeys } = await resolveServiceConfigSnapshot(options)
|
|
210
|
+
const missing = []
|
|
211
|
+
for (const field of ['serverUrl', 'appId', 'key']) {
|
|
212
|
+
if (!resolved[field]) missing.push(field)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
ok: missing.length === 0,
|
|
217
|
+
command: 'config doctor',
|
|
218
|
+
projectPath: resolved.projectPath,
|
|
219
|
+
profile: resolved.profile,
|
|
220
|
+
serverUrl: resolved.serverUrl,
|
|
221
|
+
appId: resolved.appId,
|
|
222
|
+
missing,
|
|
223
|
+
sources: {
|
|
224
|
+
project: resolved.sources.project,
|
|
225
|
+
user: resolved.sources.user,
|
|
226
|
+
key: resolved.sources.key,
|
|
227
|
+
},
|
|
228
|
+
key: {
|
|
229
|
+
present: !!resolved.key,
|
|
230
|
+
source: resolved.sources.key,
|
|
231
|
+
candidates: envKeys.key,
|
|
232
|
+
},
|
|
233
|
+
envKeys,
|
|
234
|
+
suggestions: missingConfigSuggestions(missing, envKeys),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function resolveServiceConfigSnapshot(options = {}) {
|
|
239
|
+
const projectPath = resolvePath(options.project)
|
|
240
|
+
const project = await readProjectServiceConfig(projectPath)
|
|
241
|
+
const profileName = firstValue(options.profile, process.env[ENV_KEYS.profile], project.profile)
|
|
242
|
+
const userConfig = await readUserProfileConfig(options)
|
|
243
|
+
const profile = profileName ? userConfig.profiles[profileName] || {} : {}
|
|
244
|
+
const appId = firstValue(options.appId, options.app_id, process.env[ENV_KEYS.appId], project.appId)
|
|
245
|
+
const key = resolveKey({ options, appId, profile: profileName, userKeys: userConfig.keys })
|
|
246
|
+
const resolved = normalizeConfig({
|
|
247
|
+
projectPath,
|
|
248
|
+
profile: profileName,
|
|
249
|
+
serverUrl: firstValue(options.serverUrl, options.server_url, process.env[ENV_KEYS.serverUrl], project.serverUrl, profile.serverUrl),
|
|
250
|
+
appId,
|
|
251
|
+
key: key.value,
|
|
252
|
+
sources: {
|
|
253
|
+
project: project.source,
|
|
254
|
+
user: userConfig.configPath,
|
|
255
|
+
key: key.source,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
return {
|
|
259
|
+
resolved,
|
|
260
|
+
envKeys: configEnvKeys(resolved, key.envKeys),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const serviceEnvKeys = { ...ENV_KEYS }
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createDebugPreviewDeepLink, createPreviewLaunchParams, createQrPayloadResult } from '../core/qr.js'
|
|
2
|
+
import { DiminaCliError } from '../core/errors.js'
|
|
3
|
+
import { getServiceClientVersion } from '../core/package-info.js'
|
|
4
|
+
import { resolveServiceConfig } from './service-config.js'
|
|
5
|
+
import { ServiceClient } from './service-client.js'
|
|
6
|
+
|
|
7
|
+
function normalizeBoolean(value) {
|
|
8
|
+
return value === true || value === 'true'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function versionFields(options = {}) {
|
|
12
|
+
return {
|
|
13
|
+
versionId: options.versionId || options.version_id,
|
|
14
|
+
versionNumber: options.versionNumber || options.version_number || options.version,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function createClient(options, required) {
|
|
19
|
+
const config = await resolveServiceConfig(options, { required })
|
|
20
|
+
return {
|
|
21
|
+
config,
|
|
22
|
+
client: new ServiceClient(config),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function uploadFields(config, options = {}, extra = {}) {
|
|
27
|
+
const changelog = options.changelog || options.appChangelog || options.app_changelog || options.desc || options.description
|
|
28
|
+
const launchParams = createPreviewLaunchParams(options)
|
|
29
|
+
return {
|
|
30
|
+
appId: config.appId,
|
|
31
|
+
desc: options.desc || options.description || changelog,
|
|
32
|
+
changelog,
|
|
33
|
+
clientVersion: await getServiceClientVersion(),
|
|
34
|
+
versionNumber: options.versionNumber || options.version_number || options.version,
|
|
35
|
+
preview: normalizeBoolean(options.preview),
|
|
36
|
+
submitAudit: normalizeBoolean(options.submitAudit || options.submit_audit),
|
|
37
|
+
debugPreview: false,
|
|
38
|
+
channel: options.channel,
|
|
39
|
+
proxy: options.proxy,
|
|
40
|
+
needUploadSourcemap: normalizeBoolean(options.needUploadSourcemap || options.need_upload_sourcemap || options.sourcemap) || undefined,
|
|
41
|
+
...launchParams,
|
|
42
|
+
...extra,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function previewFields(config, options = {}) {
|
|
47
|
+
return {
|
|
48
|
+
appId: config.appId,
|
|
49
|
+
...versionFields(options),
|
|
50
|
+
clientVersion: await getServiceClientVersion(),
|
|
51
|
+
channel: options.channel,
|
|
52
|
+
...createPreviewLaunchParams(options),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function withVersion(payload, version) {
|
|
57
|
+
if (!payload || version === undefined || version === null || version === '') return payload
|
|
58
|
+
const url = new URL(payload)
|
|
59
|
+
if (!url.searchParams.has('version')) url.searchParams.set('version', String(version))
|
|
60
|
+
return url.toString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function withTrialQr(response, version) {
|
|
64
|
+
const payload = withVersion(response?.preview?.scanUrl || response?.scanUrl, version)
|
|
65
|
+
return payload ? { ...response, ...createQrPayloadResult(payload) } : response
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createServiceProvider() {
|
|
69
|
+
const provider = {
|
|
70
|
+
login: async () => {
|
|
71
|
+
throw new DiminaCliError('DIMINA_PROVIDER_UNSUPPORTED', 'login is not implemented for the signed service provider.', {
|
|
72
|
+
command: 'login',
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
upload: async ({ artifact, options }) => {
|
|
77
|
+
const { config, client } = await createClient(options)
|
|
78
|
+
const fields = await uploadFields(config, options)
|
|
79
|
+
const response = await client.uploadPackage(artifact.packagePath, fields)
|
|
80
|
+
return withTrialQr(response, artifact.config?.versionName)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
debugPreview: async ({ artifact, options }) => {
|
|
84
|
+
if (normalizeBoolean(options.preview) || normalizeBoolean(options.submitAudit || options.submit_audit)) {
|
|
85
|
+
throw new DiminaCliError('NEUX_CLI_DEBUG_PREVIEW_FLAGS_INVALID', 'debug-preview cannot be combined with preview or submit-audit.', {
|
|
86
|
+
preview: normalizeBoolean(options.preview),
|
|
87
|
+
submitAudit: normalizeBoolean(options.submitAudit || options.submit_audit),
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { config, client } = await createClient(options)
|
|
92
|
+
const response = await client.uploadPackage(artifact.packagePath, await uploadFields(config, options, {
|
|
93
|
+
preview: false,
|
|
94
|
+
submitAudit: false,
|
|
95
|
+
debugPreview: true,
|
|
96
|
+
}))
|
|
97
|
+
const deepLink = createDebugPreviewDeepLink({
|
|
98
|
+
appId: config.appId,
|
|
99
|
+
name: artifact.config?.name,
|
|
100
|
+
version: artifact.config?.versionName,
|
|
101
|
+
download: response.packageDownloadUrl,
|
|
102
|
+
...createPreviewLaunchParams(options),
|
|
103
|
+
})
|
|
104
|
+
return {
|
|
105
|
+
...response,
|
|
106
|
+
...createQrPayloadResult(deepLink),
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
servicePreview: async ({ options }) => {
|
|
111
|
+
const { config, client } = await createClient(options)
|
|
112
|
+
return withTrialQr(await client.postJson('/preview', await previewFields(config, options)))
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
submitAudit: async ({ options }) => {
|
|
116
|
+
const { config, client } = await createClient(options)
|
|
117
|
+
return await client.postJson('/submit-audit', {
|
|
118
|
+
appId: config.appId,
|
|
119
|
+
...versionFields(options),
|
|
120
|
+
channel: options.channel,
|
|
121
|
+
host: options.host,
|
|
122
|
+
autoPublish: normalizeBoolean(options.autoPublish || options.auto_publish) || undefined,
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
version: async ({ options }) => {
|
|
127
|
+
const { config, client } = await createClient(options, ['serverUrl', 'appId', 'key'])
|
|
128
|
+
return await client.get('/version', { appId: config.appId })
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
auditStatus: async ({ options }) => {
|
|
132
|
+
const { config, client } = await createClient(options)
|
|
133
|
+
const versionId = options.versionId || options.version_id
|
|
134
|
+
if (!versionId) {
|
|
135
|
+
throw new DiminaCliError('NEUX_CLI_CONFIG_MISSING', 'Missing required option: versionId', {
|
|
136
|
+
missing: ['versionId'],
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return await client.get('/audit-status', { appId: config.appId, versionId })
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
provider.audit = provider.submitAudit
|
|
144
|
+
provider.submit = provider.submitAudit
|
|
145
|
+
return provider
|
|
146
|
+
}
|