packing-ecom 2026.6.17
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 +39 -0
- package/bin/packing-ecom.js +220 -0
- package/lib/installer.js +91 -0
- package/lib/platform.js +67 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# packing-ecom
|
|
2
|
+
|
|
3
|
+
CLI cài đặt và khởi động E-commerce Packing Control Center.
|
|
4
|
+
|
|
5
|
+
## Nền tảng hỗ trợ
|
|
6
|
+
|
|
7
|
+
- Windows 10/11 x64
|
|
8
|
+
- macOS Intel x64
|
|
9
|
+
- macOS Apple Silicon
|
|
10
|
+
- Ubuntu 22.04/24.04 x64
|
|
11
|
+
|
|
12
|
+
## Cài đặt
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g packing-ecom
|
|
16
|
+
packing-ecom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Khi cài đặt, npm tự chọn binary package phù hợp với hệ điều hành/CPU. Lệnh
|
|
20
|
+
`packing-ecom` xác minh binary, khởi động backend cục bộ, tự chọn cổng từ 8000
|
|
21
|
+
đến 8999 và mở giao diện trong trình duyệt. Python, OpenCV và các thư viện xử
|
|
22
|
+
lý ảnh đã nằm trong binary; máy người dùng không cần cài Python hay Docker.
|
|
23
|
+
|
|
24
|
+
Ứng dụng có thể sử dụng camera tích hợp/USB và URL camera RTSP/HTTP trong cùng
|
|
25
|
+
mạng LAN. Trên macOS, người dùng cần cấp quyền Camera cho Terminal; trên Linux,
|
|
26
|
+
tài khoản cần có quyền truy cập thiết bị `/dev/video*`.
|
|
27
|
+
|
|
28
|
+
## Lệnh
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
packing-ecom --help
|
|
32
|
+
packing-ecom --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Video và ảnh mặc định được lưu tại `Downloads/e-commerce-packing`. Có thể đổi
|
|
36
|
+
thư mục lưu trữ trong giao diện.
|
|
37
|
+
|
|
38
|
+
Package chính chỉ chứa launcher. Binary được phân phối bằng package npm riêng
|
|
39
|
+
cho từng nền tảng và được xác minh bằng SHA-256 trước khi chạy.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const childProcess = require('node:child_process')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const net = require('node:net')
|
|
7
|
+
const os = require('node:os')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
|
|
10
|
+
const packageJson = require('../package.json')
|
|
11
|
+
const { ensureBinary } = require('../lib/installer')
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`packing-ecom ${packageJson.version}
|
|
15
|
+
|
|
16
|
+
Khởi động E-commerce Packing Control Center.
|
|
17
|
+
|
|
18
|
+
Cách dùng:
|
|
19
|
+
packing-ecom
|
|
20
|
+
packing-ecom --help
|
|
21
|
+
packing-ecom --version`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStateDirectory() {
|
|
25
|
+
if (process.env.PACKING_ECOM_STATE_DIR) {
|
|
26
|
+
return path.resolve(process.env.PACKING_ECOM_STATE_DIR)
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
return path.join(
|
|
30
|
+
process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(),
|
|
31
|
+
'packing-ecom',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (process.platform === 'darwin') {
|
|
35
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'packing-ecom')
|
|
36
|
+
}
|
|
37
|
+
return path.join(
|
|
38
|
+
process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'),
|
|
39
|
+
'packing-ecom',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isProcessRunning(pid) {
|
|
44
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 0)
|
|
47
|
+
return true
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return error.code === 'EPERM'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function openBrowser(url) {
|
|
54
|
+
let command
|
|
55
|
+
let args
|
|
56
|
+
if (process.platform === 'darwin') {
|
|
57
|
+
command = 'open'
|
|
58
|
+
args = [url]
|
|
59
|
+
} else if (process.platform === 'win32') {
|
|
60
|
+
command = 'cmd.exe'
|
|
61
|
+
args = ['/d', '/s', '/c', 'start', '', url]
|
|
62
|
+
} else {
|
|
63
|
+
command = 'xdg-open'
|
|
64
|
+
args = [url]
|
|
65
|
+
}
|
|
66
|
+
const opener = childProcess.spawn(command, args, {
|
|
67
|
+
detached: true,
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
})
|
|
71
|
+
opener.unref()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readActiveInstance(lockPath) {
|
|
75
|
+
try {
|
|
76
|
+
const state = JSON.parse(fs.readFileSync(lockPath, 'utf8'))
|
|
77
|
+
return isProcessRunning(state.pid) ? state : null
|
|
78
|
+
} catch {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function acquireLock(lockPath, port) {
|
|
84
|
+
const active = readActiveInstance(lockPath)
|
|
85
|
+
if (active) return { active, acquired: false }
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(lockPath)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code !== 'ENOENT') throw error
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const fileDescriptor = fs.openSync(lockPath, 'wx', 0o600)
|
|
95
|
+
fs.writeFileSync(fileDescriptor, JSON.stringify({ pid: process.pid, port }))
|
|
96
|
+
fs.closeSync(fileDescriptor)
|
|
97
|
+
return { active: null, acquired: true }
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error.code !== 'EEXIST') throw error
|
|
100
|
+
return { active: readActiveInstance(lockPath), acquired: false }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isPortAvailable(port) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const server = net.createServer()
|
|
107
|
+
server.unref()
|
|
108
|
+
server.once('error', () => resolve(false))
|
|
109
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
|
|
110
|
+
server.close(() => resolve(true))
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function findFreePort(startPort = 8000) {
|
|
116
|
+
const requestedPort = Number.parseInt(process.env.PACKING_ECOM_PORT || '', 10)
|
|
117
|
+
if (Number.isInteger(requestedPort) && requestedPort >= 1024 && requestedPort <= 65535) {
|
|
118
|
+
if (await isPortAvailable(requestedPort)) return requestedPort
|
|
119
|
+
throw new Error(`Cổng PACKING_ECOM_PORT=${requestedPort} đang được sử dụng.`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let port = startPort; port < 9000; port += 1) {
|
|
123
|
+
if (await isPortAvailable(port)) return port
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Không tìm được cổng trống trong khoảng 8000-8999.')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function run() {
|
|
129
|
+
const args = process.argv.slice(2)
|
|
130
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
131
|
+
printHelp()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
135
|
+
console.log(packageJson.version)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (args.length > 0) {
|
|
139
|
+
throw new Error(`Tham số không được hỗ trợ: ${args.join(' ')}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const stateDirectory = getStateDirectory()
|
|
143
|
+
fs.mkdirSync(stateDirectory, { recursive: true })
|
|
144
|
+
const lockPath = path.join(stateDirectory, 'packing-ecom.lock')
|
|
145
|
+
|
|
146
|
+
const currentInstance = readActiveInstance(lockPath)
|
|
147
|
+
if (currentInstance) {
|
|
148
|
+
const url = `http://127.0.0.1:${currentInstance.port}`
|
|
149
|
+
console.log(`[packing-ecom] Ứng dụng đang chạy tại ${url}`)
|
|
150
|
+
openBrowser(url)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const port = await findFreePort()
|
|
155
|
+
const lock = acquireLock(lockPath, port)
|
|
156
|
+
if (!lock.acquired) {
|
|
157
|
+
if (!lock.active) {
|
|
158
|
+
throw new Error('Không thể giành quyền khởi động ứng dụng. Vui lòng chạy lại.')
|
|
159
|
+
}
|
|
160
|
+
const url = `http://127.0.0.1:${lock.active.port}`
|
|
161
|
+
console.log(`[packing-ecom] Ứng dụng đang chạy tại ${url}`)
|
|
162
|
+
openBrowser(url)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const cleanup = () => {
|
|
167
|
+
try {
|
|
168
|
+
const state = JSON.parse(fs.readFileSync(lockPath, 'utf8'))
|
|
169
|
+
if (state.pid === process.pid) fs.unlinkSync(lockPath)
|
|
170
|
+
} catch {
|
|
171
|
+
// Lock đã được dọn hoặc không còn hợp lệ.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const binaryPath = await ensureBinary()
|
|
177
|
+
console.log(`[packing-ecom] Khởi động tại http://127.0.0.1:${port}`)
|
|
178
|
+
const child = childProcess.spawn(binaryPath, [], {
|
|
179
|
+
env: {
|
|
180
|
+
...process.env,
|
|
181
|
+
IS_DOCKER: '0',
|
|
182
|
+
PORT: String(port),
|
|
183
|
+
PACKING_ECOM_LOG_DIR: stateDirectory,
|
|
184
|
+
},
|
|
185
|
+
stdio: 'inherit',
|
|
186
|
+
windowsHide: false,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const forwardSignal = (signal) => {
|
|
190
|
+
if (!child.killed) child.kill(signal)
|
|
191
|
+
}
|
|
192
|
+
process.once('SIGINT', () => forwardSignal('SIGINT'))
|
|
193
|
+
process.once('SIGTERM', () => forwardSignal('SIGTERM'))
|
|
194
|
+
|
|
195
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
196
|
+
child.once('error', reject)
|
|
197
|
+
child.once('exit', (code, signal) => {
|
|
198
|
+
if (signal) resolve(1)
|
|
199
|
+
else resolve(code || 0)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
process.exitCode = exitCode
|
|
203
|
+
} finally {
|
|
204
|
+
cleanup()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
run().catch((error) => {
|
|
209
|
+
console.error(`[packing-ecom] ${error.message}`)
|
|
210
|
+
process.exitCode = 1
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
acquireLock,
|
|
215
|
+
findFreePort,
|
|
216
|
+
getStateDirectory,
|
|
217
|
+
isProcessRunning,
|
|
218
|
+
isPortAvailable,
|
|
219
|
+
readActiveInstance,
|
|
220
|
+
}
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getLocalExecutableName,
|
|
9
|
+
getPlatformPackageName,
|
|
10
|
+
getTarget,
|
|
11
|
+
} = require('./platform')
|
|
12
|
+
|
|
13
|
+
function parseChecksum(contents) {
|
|
14
|
+
const match = contents.match(/\b([a-f0-9]{64})\b/i)
|
|
15
|
+
if (!match) {
|
|
16
|
+
throw new Error('Checksum SHA-256 của binary package không hợp lệ.')
|
|
17
|
+
}
|
|
18
|
+
return match[1].toLowerCase()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function calculateSha256(filePath) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const hash = crypto.createHash('sha256')
|
|
24
|
+
const stream = fs.createReadStream(filePath)
|
|
25
|
+
stream.on('error', reject)
|
|
26
|
+
stream.on('data', (chunk) => hash.update(chunk))
|
|
27
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveBinaryPath(target = getTarget()) {
|
|
32
|
+
if (process.env.PACKING_ECOM_BINARY_PATH) {
|
|
33
|
+
return path.resolve(process.env.PACKING_ECOM_BINARY_PATH)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const packageName = getPlatformPackageName(target)
|
|
37
|
+
let packageJsonPath
|
|
38
|
+
try {
|
|
39
|
+
packageJsonPath = require.resolve(`${packageName}/package.json`, {
|
|
40
|
+
paths: [path.resolve(__dirname, '..')],
|
|
41
|
+
})
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Không tìm thấy binary package ${packageName}. ` +
|
|
45
|
+
'Hãy cài lại packing-ecom và không dùng --omit=optional.',
|
|
46
|
+
{ cause: error },
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return path.join(
|
|
51
|
+
path.dirname(packageJsonPath),
|
|
52
|
+
'bin',
|
|
53
|
+
getLocalExecutableName(target),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function ensureBinary() {
|
|
58
|
+
const binaryPath = resolveBinaryPath()
|
|
59
|
+
if (!fs.existsSync(binaryPath)) {
|
|
60
|
+
throw new Error(`Không tìm thấy binary: ${binaryPath}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (process.env.PACKING_ECOM_BINARY_PATH) {
|
|
64
|
+
return binaryPath
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const checksumPath = `${binaryPath}.sha256`
|
|
68
|
+
if (!fs.existsSync(checksumPath)) {
|
|
69
|
+
throw new Error(`Không tìm thấy checksum: ${checksumPath}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const expected = parseChecksum(fs.readFileSync(checksumPath, 'utf8'))
|
|
73
|
+
const actual = await calculateSha256(binaryPath)
|
|
74
|
+
if (actual !== expected) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Checksum SHA-256 không khớp cho ${path.basename(binaryPath)}.`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.platform !== 'win32') {
|
|
81
|
+
fs.chmodSync(binaryPath, 0o755)
|
|
82
|
+
}
|
|
83
|
+
return binaryPath
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
calculateSha256,
|
|
88
|
+
ensureBinary,
|
|
89
|
+
parseChecksum,
|
|
90
|
+
resolveBinaryPath,
|
|
91
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_TARGETS = Object.freeze({
|
|
4
|
+
'win32-x64': {
|
|
5
|
+
extension: '.exe',
|
|
6
|
+
label: 'Windows 10/11 x64',
|
|
7
|
+
packageName: 'packing-ecom-windows-x64',
|
|
8
|
+
},
|
|
9
|
+
'darwin-x64': {
|
|
10
|
+
extension: '',
|
|
11
|
+
label: 'macOS Intel x64',
|
|
12
|
+
packageName: 'packing-ecom-darwin-x64',
|
|
13
|
+
},
|
|
14
|
+
'darwin-arm64': {
|
|
15
|
+
extension: '',
|
|
16
|
+
label: 'macOS Apple Silicon',
|
|
17
|
+
packageName: 'packing-ecom-darwin-arm64',
|
|
18
|
+
},
|
|
19
|
+
'linux-x64': {
|
|
20
|
+
extension: '',
|
|
21
|
+
label: 'Linux x64 (Ubuntu 22.04/24.04)',
|
|
22
|
+
packageName: 'packing-ecom-linux-x64',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function getTarget(platform = process.platform, arch = process.arch) {
|
|
27
|
+
const target = `${platform}-${arch}`
|
|
28
|
+
if (!SUPPORTED_TARGETS[target]) {
|
|
29
|
+
const supported = Object.values(SUPPORTED_TARGETS)
|
|
30
|
+
.map(({ label }) => label)
|
|
31
|
+
.join(', ')
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Hệ điều hành/CPU ${target} chưa được hỗ trợ. Các nền tảng hỗ trợ: ${supported}.`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return target
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getReleaseFilename(target) {
|
|
40
|
+
const config = SUPPORTED_TARGETS[target]
|
|
41
|
+
if (!config) {
|
|
42
|
+
throw new Error(`Target build không hợp lệ: ${target}`)
|
|
43
|
+
}
|
|
44
|
+
return `packing-ecom-${target}${config.extension}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLocalExecutableName(target) {
|
|
48
|
+
return target.startsWith('win32-')
|
|
49
|
+
? 'packing-ecom.exe'
|
|
50
|
+
: 'packing-ecom'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getPlatformPackageName(target) {
|
|
54
|
+
const config = SUPPORTED_TARGETS[target]
|
|
55
|
+
if (!config) {
|
|
56
|
+
throw new Error(`Target build không hợp lệ: ${target}`)
|
|
57
|
+
}
|
|
58
|
+
return config.packageName
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
SUPPORTED_TARGETS,
|
|
63
|
+
getLocalExecutableName,
|
|
64
|
+
getPlatformPackageName,
|
|
65
|
+
getReleaseFilename,
|
|
66
|
+
getTarget,
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "packing-ecom",
|
|
3
|
+
"version": "2026.6.17",
|
|
4
|
+
"description": "E-commerce packing camera control center",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/JustinNguyen9979/e-commerce-packing.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/JustinNguyen9979/e-commerce-packing",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/JustinNguyen9979/e-commerce-packing/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"packing-ecom": "bin/packing-ecom.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"lib/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test ../../tests/npm/*.test.js"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"optionalDependencies": {
|
|
29
|
+
"packing-ecom-darwin-arm64": "2026.6.17",
|
|
30
|
+
"packing-ecom-darwin-x64": "2026.6.17",
|
|
31
|
+
"packing-ecom-linux-x64": "2026.6.17",
|
|
32
|
+
"packing-ecom-windows-x64": "2026.6.17"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|