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 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
+ }
@@ -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
+ }
@@ -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
+ }