@zsukim/ctv-run 1.0.10 → 1.0.11
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 +7 -5
- package/dist/icon.png +0 -0
- package/dist/platforms/tizen.js +101 -60
- package/dist/utils/platform.js +13 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
|
|
19
19
|
## 실행 모드 개요
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
플랫폼은 두 가지 모드를 지원합니다.
|
|
22
22
|
|
|
23
|
-
| 모드 | 옵션 | 설명
|
|
24
|
-
|
|
25
|
-
| **Live Mode** | (기본값) | TV가 로드할 URL을 TV에 주입. HMR 지원 |
|
|
26
|
-
| **Static Mode** | `--target=dist` | 빌드 결과물을 TV에 직접 설치 |
|
|
23
|
+
| 모드 | 옵션 | 설명 |
|
|
24
|
+
|------|------|----------------------------------------|
|
|
25
|
+
| **Live Mode** | (기본값) | TV가 로드할 URL을 TV에 주입. HMR 지원 (Tizen 제외) |
|
|
26
|
+
| **Static Mode** | `--target=dist` | 빌드 결과물을 TV에 직접 설치 (Vizio 제외) |
|
|
27
27
|
|
|
28
28
|
> **Vizio**는 구조상 항상 URL이 필요하므로 Live Mode만 지원합니다.
|
|
29
|
+
>
|
|
30
|
+
> **Tizen** Live Mode는 HMR을 지원하지 않습니다. Samsung Tizen TV의 구형 WebKit이 ES native modules(`type="module"`)를 지원하지 않아 `vite dev` 서버에 접속하면 검은 화면이 나타납니다. `vite preview --host`를 사용하세요.
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
package/dist/icon.png
ADDED
|
Binary file
|
package/dist/platforms/tizen.js
CHANGED
|
@@ -30,14 +30,30 @@ function runTizen() {
|
|
|
30
30
|
console.error('\n❌ Tizen TV IP 주소가 필요합니다! (예: --ip=192.168.x.x)');
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const resolveBin = (name, fallbackPaths) => {
|
|
34
|
+
try {
|
|
35
|
+
(0, child_process_1.execSync)(`which ${name}`, { stdio: 'ignore' });
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
catch (_a) {
|
|
39
|
+
return fallbackPaths.find((p) => fs_extra_1.default.existsSync(p)) || name;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const tizenBin = getArg('tizenPath') ||
|
|
43
|
+
((_b = config.tizen) === null || _b === void 0 ? void 0 : _b.tizenPath) ||
|
|
44
|
+
resolveBin('tizen', [
|
|
45
|
+
`${process.env.HOME}/tizen-studio/tools/ide/bin/tizen`,
|
|
46
|
+
]);
|
|
47
|
+
const sdbBin = getArg('sdbPath') ||
|
|
48
|
+
((_c = config.tizen) === null || _c === void 0 ? void 0 : _c.sdbPath) ||
|
|
49
|
+
resolveBin('sdb', [`${process.env.HOME}/tizen-studio/tools/sdb`]);
|
|
35
50
|
const targetArg = getArg('target');
|
|
36
51
|
const isLiveMode = !targetArg;
|
|
37
|
-
const port = getArg('port') ||
|
|
38
|
-
|
|
39
|
-
((
|
|
40
|
-
|
|
52
|
+
const port = getArg('port') ||
|
|
53
|
+
((_d = config.tizen) === null || _d === void 0 ? void 0 : _d.port) ||
|
|
54
|
+
((_e = config.common) === null || _e === void 0 ? void 0 : _e.devServerPort) ||
|
|
55
|
+
3000;
|
|
56
|
+
const appUrl = getArg('url') || ((_f = config.tizen) === null || _f === void 0 ? void 0 : _f.url) || `http://${(0, ip_1.getLocalIp)()}:${port}`;
|
|
41
57
|
const tempAppDir = path_1.default.resolve(process.cwd(), '.ctv-run-tizen');
|
|
42
58
|
if (fs_extra_1.default.existsSync(tempAppDir))
|
|
43
59
|
fs_extra_1.default.removeSync(tempAppDir);
|
|
@@ -46,16 +62,8 @@ function runTizen() {
|
|
|
46
62
|
console.log(`🚀 [${isLiveMode ? 'Live Mode' : 'Static Mode'}] 준비 중...`);
|
|
47
63
|
// 콘텐츠 준비 및 기존 config.xml 탐색
|
|
48
64
|
if (isLiveMode) {
|
|
49
|
-
// Live Mode:
|
|
50
|
-
|
|
51
|
-
path_1.default.resolve(process.cwd(), 'config.xml'),
|
|
52
|
-
path_1.default.resolve(process.cwd(), 'public', 'config.xml'),
|
|
53
|
-
];
|
|
54
|
-
const foundPath = searchPaths.find((p) => fs_extra_1.default.existsSync(p));
|
|
55
|
-
if (foundPath)
|
|
56
|
-
fs_extra_1.default.copySync(foundPath, path_1.default.join(tempAppDir, 'config.xml'));
|
|
57
|
-
const redirectHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="background:#000;"><script type="text/javascript">window.location.replace("${appUrl}");</script></body></html>`;
|
|
58
|
-
fs_extra_1.default.writeFileSync(path_1.default.join(tempAppDir, 'index.html'), redirectHtml);
|
|
65
|
+
// Live Mode: index.html은 TV가 content src URL로 바로 접속하므로 불필요
|
|
66
|
+
// config.xml의 content src에 개발서버 URL을 직접 지정
|
|
59
67
|
}
|
|
60
68
|
else {
|
|
61
69
|
// Static Mode: 빌드 타겟 폴더 복사
|
|
@@ -73,56 +81,81 @@ function runTizen() {
|
|
|
73
81
|
((_g = config.tizen) === null || _g === void 0 ? void 0 : _g.appId) ||
|
|
74
82
|
'A123456789.CtvRunApp';
|
|
75
83
|
const safeAppName = ((xmlInfo === null || xmlInfo === void 0 ? void 0 : xmlInfo.appName) || 'CtvRunApp').replace(/\s+/g, '');
|
|
76
|
-
// config.xml
|
|
77
|
-
yield ensureTizenFiles(appId, tempAppDir, safeAppName);
|
|
84
|
+
// static mode는 dist의 config.xml 유지, live mode는 개발서버 URL로 새로 생성
|
|
85
|
+
yield ensureTizenFiles(appId, tempAppDir, safeAppName, !isLiveMode, isLiveMode ? appUrl : '');
|
|
78
86
|
// Tizen 인증서 프로필 확인/생성/활성화
|
|
79
87
|
console.log('\n🎫 Tizen 인증서 프로필 확인 중...');
|
|
80
|
-
const
|
|
88
|
+
const profilesXmlPath = path_1.default.join(process.env.HOME || '', 'tizen-studio-data/profile/profiles.xml');
|
|
89
|
+
const readProfilesXml = () => {
|
|
81
90
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
91
|
+
if (!fs_extra_1.default.existsSync(profilesXmlPath))
|
|
92
|
+
return { active: '', profiles: [] };
|
|
93
|
+
const xml = fs_extra_1.default.readFileSync(profilesXmlPath, 'utf8');
|
|
94
|
+
const activeMatch = xml.match(/<profiles[^>]*active="([^"]+)"/);
|
|
95
|
+
const profileMatches = [...xml.matchAll(/<profile\s+name="([^"]+)"/g)];
|
|
96
|
+
return {
|
|
97
|
+
active: activeMatch ? activeMatch[1] : '',
|
|
98
|
+
profiles: profileMatches.map((m) => m[1]),
|
|
99
|
+
};
|
|
87
100
|
}
|
|
88
101
|
catch (e) {
|
|
89
|
-
return '';
|
|
102
|
+
return { active: '', profiles: [] };
|
|
90
103
|
}
|
|
91
104
|
};
|
|
92
|
-
|
|
105
|
+
const setActiveProfile = (name) => {
|
|
106
|
+
(0, child_process_1.execSync)(`${tizenBin} security-profiles set-active -n "${name}"`, {
|
|
107
|
+
stdio: 'inherit',
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
let activeProfile = readProfilesXml().active;
|
|
93
111
|
if (!activeProfile) {
|
|
94
|
-
console.log('⚠️ 활성화된 프로필이 없습니다. 앱 전용 인증서를 세팅합니다.');
|
|
95
112
|
const askQuestion = (query) => {
|
|
96
113
|
const buffer = Buffer.alloc(1024);
|
|
97
114
|
process.stdout.write(query);
|
|
98
115
|
const bytesRead = fs_extra_1.default.readSync(0, buffer, 0, 1024, null);
|
|
99
116
|
return buffer.toString('utf8', 0, bytesRead).trim();
|
|
100
117
|
};
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (fs_extra_1.default.existsSync(p12Path)) {
|
|
108
|
-
console.log(`✨ 기존 인증서 발견: ${p12Path}`);
|
|
109
|
-
p12Pwd = askQuestion('🔑 인증서 비밀번호를 입력하세요: ');
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
p12Pwd = askQuestion('🔑 새로 만들 인증서 비밀번호(4자 이상): ');
|
|
113
|
-
(0, child_process_1.execSync)(`${tizenBin} certificate -a "${certFileName}" -p "${p12Pwd}" -f "${certFileName}"`, { stdio: 'inherit' });
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
(0, child_process_1.execSync)(`${tizenBin} security-profiles remove -n "${profileName}"`, {
|
|
117
|
-
stdio: 'ignore',
|
|
118
|
+
// 등록된 프로필 목록 확인
|
|
119
|
+
const existingProfiles = readProfilesXml().profiles;
|
|
120
|
+
if (existingProfiles.length > 0) {
|
|
121
|
+
console.log('⚠️ 활성화된 프로필이 없습니다. 등록된 프로필 목록:');
|
|
122
|
+
existingProfiles.forEach((p, i) => {
|
|
123
|
+
console.log(` ${i + 1}. ${p}`);
|
|
118
124
|
});
|
|
125
|
+
const answer = askQuestion('🔑 사용할 프로필 번호를 입력하세요 (새로운 ctv-run-cert 생성): ');
|
|
126
|
+
const idx = parseInt(answer, 10) - 1;
|
|
127
|
+
if (!isNaN(idx) && idx >= 0 && idx < existingProfiles.length) {
|
|
128
|
+
const chosen = existingProfiles[idx];
|
|
129
|
+
setActiveProfile(chosen);
|
|
130
|
+
activeProfile = readProfilesXml().active;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!activeProfile) {
|
|
134
|
+
console.log('⚠️ 활성화된 프로필이 없습니다. 앱 전용 인증서를 세팅합니다.');
|
|
135
|
+
const certFileName = 'ctv-run-cert';
|
|
136
|
+
const profileName = 'ctv-run-profile';
|
|
137
|
+
const homeDir = process.env.HOME || '';
|
|
138
|
+
const defaultCertDir = path_1.default.join(homeDir, 'tizen-studio-data/keystore/author');
|
|
139
|
+
const p12Path = path_1.default.join(defaultCertDir, `${certFileName}.p12`);
|
|
140
|
+
let p12Pwd = '';
|
|
141
|
+
if (fs_extra_1.default.existsSync(p12Path)) {
|
|
142
|
+
console.log(`✨ 기존 인증서 발견: ${p12Path}`);
|
|
143
|
+
p12Pwd = askQuestion('🔑 인증서 비밀번호를 입력하세요: ');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
p12Pwd = askQuestion('🔑 새로 만들 인증서 비밀번호(4자 이상): ');
|
|
147
|
+
(0, child_process_1.execSync)(`${tizenBin} certificate -a "${certFileName}" -p "${p12Pwd}" -f "${certFileName}"`, { stdio: 'inherit' });
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
(0, child_process_1.execSync)(`${tizenBin} security-profiles remove -n "${profileName}"`, {
|
|
151
|
+
stdio: 'ignore',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (e) { }
|
|
155
|
+
(0, child_process_1.execSync)(`${tizenBin} security-profiles add -n "${profileName}" -a "${p12Path}" -p "${p12Pwd}"`, { stdio: 'inherit' });
|
|
156
|
+
setActiveProfile(profileName);
|
|
157
|
+
activeProfile = readProfilesXml().active;
|
|
119
158
|
}
|
|
120
|
-
catch (e) { }
|
|
121
|
-
(0, child_process_1.execSync)(`${tizenBin} security-profiles add -n "${profileName}" -a "${p12Path}" -p "${p12Pwd}"`, { stdio: 'inherit' });
|
|
122
|
-
(0, child_process_1.execSync)(`${tizenBin} security-profiles set-active -n "${profileName}"`, {
|
|
123
|
-
stdio: 'inherit',
|
|
124
|
-
});
|
|
125
|
-
activeProfile = getActive();
|
|
126
159
|
}
|
|
127
160
|
// 패키징
|
|
128
161
|
console.log(`\n🔨 [${activeProfile}] 프로필로 패키징 중...`);
|
|
@@ -151,7 +184,7 @@ function runTizen() {
|
|
|
151
184
|
return buffer.toString('utf8', 0, bytesRead).trim().toLowerCase();
|
|
152
185
|
};
|
|
153
186
|
const answer = askQuestion('❓ 기존 앱을 삭제하고 다시 설치할까요? (Y/n): ');
|
|
154
|
-
if (answer === '
|
|
187
|
+
if (answer === 'y') {
|
|
155
188
|
console.log(`🗑️ 기존 앱 삭제 중: ${appId}`);
|
|
156
189
|
(0, child_process_1.execSync)(`${tizenBin} uninstall -p ${appId}`, { stdio: 'inherit' });
|
|
157
190
|
console.log('✅ 삭제 완료. 재설치 시도...');
|
|
@@ -171,8 +204,7 @@ function runTizen() {
|
|
|
171
204
|
console.error(`\n❌ 오류 발생: ${err.message}`);
|
|
172
205
|
}
|
|
173
206
|
finally {
|
|
174
|
-
if (
|
|
175
|
-
fs_extra_1.default.removeSync(tempAppDir);
|
|
207
|
+
// if (fs.existsSync(tempAppDir)) fs.removeSync(tempAppDir);
|
|
176
208
|
}
|
|
177
209
|
});
|
|
178
210
|
}
|
|
@@ -198,10 +230,15 @@ function getInfoFromXml(dir) {
|
|
|
198
230
|
}
|
|
199
231
|
return null;
|
|
200
232
|
}
|
|
201
|
-
function ensureTizenFiles(
|
|
202
|
-
return __awaiter(this,
|
|
233
|
+
function ensureTizenFiles(appId_1, tempDir_1, appName_1) {
|
|
234
|
+
return __awaiter(this, arguments, void 0, function* (appId, tempDir, appName, keepExistingConfig = false, liveUrl = '') {
|
|
203
235
|
const configXmlPath = path_1.default.join(tempDir, 'config.xml');
|
|
204
|
-
|
|
236
|
+
if (keepExistingConfig && fs_extra_1.default.existsSync(configXmlPath)) {
|
|
237
|
+
// static mode: dist의 config.xml을 그대로 유지
|
|
238
|
+
(0, platform_1.resolveIcon)(tempDir);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// live mode: 기존 config.xml 삭제 후 새로 생성
|
|
205
242
|
if (fs_extra_1.default.existsSync(configXmlPath)) {
|
|
206
243
|
fs_extra_1.default.removeSync(configXmlPath);
|
|
207
244
|
}
|
|
@@ -210,19 +247,23 @@ function ensureTizenFiles(appId, tempDir, appName) {
|
|
|
210
247
|
const packageId = appId.split('.')[0] || 'TizenApp';
|
|
211
248
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
212
249
|
<widget xmlns:tizen="http://tizen.org/ns/widgets" xmlns="http://www.w3.org/ns/widgets" id="http://yourdomain/tizen/${appName}" version="1.0.0" viewmodes="maximized">
|
|
213
|
-
<tizen:application id="${appId}" package="${packageId}" required_version="2.
|
|
214
|
-
<tizen:profile name="tv-samsung"/>
|
|
250
|
+
<tizen:application id="${appId}" package="${packageId}" required_version="2.1"/>
|
|
215
251
|
<content src="index.html"/>
|
|
216
|
-
<name
|
|
252
|
+
<feature name="http://tizen.org/feature/screen.size.normal.1080.1920"/>
|
|
217
253
|
<icon src="icon.png"/>
|
|
254
|
+
<name>${appName}</name>
|
|
255
|
+
<tizen:profile name="tv-samsung"/>
|
|
218
256
|
<access origin="*" subdomains="true"/>
|
|
219
|
-
<tizen:allow-navigation>*</tizen:allow-navigation>
|
|
220
257
|
<tizen:privilege name="http://tizen.org/privilege/internet"/>
|
|
221
258
|
<tizen:privilege name="http://developer.samsung.com/privilege/network.public"/>
|
|
222
259
|
<tizen:privilege name="http://tizen.org/privilege/tv.inputdevice" />
|
|
223
260
|
<tizen:setting screen-orientation="landscape" context-menu="enable" background-support="disable" encryption="disable" install-location="auto" hwkey-event="enable"/>
|
|
224
261
|
</widget>`;
|
|
225
262
|
fs_extra_1.default.writeFileSync(configXmlPath, xmlContent, 'utf8');
|
|
263
|
+
if (liveUrl) {
|
|
264
|
+
const redirectHtml = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Loading...</title><meta http-equiv="refresh" content="0; url=${liveUrl}"></head><body style="background:#000;"></body></html>`;
|
|
265
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(tempDir, 'index.html'), redirectHtml);
|
|
266
|
+
}
|
|
226
267
|
(0, platform_1.resolveIcon)(tempDir);
|
|
227
268
|
});
|
|
228
269
|
}
|
package/dist/utils/platform.js
CHANGED
|
@@ -12,15 +12,26 @@ function makeArgParser(args) {
|
|
|
12
12
|
}
|
|
13
13
|
function resolveIcon(tempDir) {
|
|
14
14
|
const targetIconPath = path_1.default.join(tempDir, 'icon.png');
|
|
15
|
-
const
|
|
15
|
+
const iconSearchPaths = [
|
|
16
|
+
path_1.default.resolve(process.cwd(), 'src', 'assets', 'icon.png'),
|
|
17
|
+
path_1.default.resolve(process.cwd(), 'public', 'icon.png'),
|
|
18
|
+
path_1.default.resolve(process.cwd(), 'icon.png'),
|
|
19
|
+
path_1.default.resolve(process.cwd(), 'assets', 'icon.png'),
|
|
20
|
+
];
|
|
21
|
+
const sourceIconPath = iconSearchPaths.find((p) => fs_extra_1.default.existsSync(p));
|
|
16
22
|
if (fs_extra_1.default.existsSync(targetIconPath)) {
|
|
17
23
|
console.log('✅ 기존 프로젝트의 아이콘을 유지합니다: icon.png');
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
|
-
|
|
26
|
+
const fallbackIconPath = path_1.default.join(__dirname, '../icon.png');
|
|
27
|
+
if (sourceIconPath) {
|
|
21
28
|
fs_extra_1.default.copySync(sourceIconPath, targetIconPath);
|
|
22
29
|
console.log('🖼 기본 아이콘을 가져왔습니다.');
|
|
23
30
|
}
|
|
31
|
+
else if (fs_extra_1.default.existsSync(fallbackIconPath)) {
|
|
32
|
+
fs_extra_1.default.copySync(fallbackIconPath, targetIconPath);
|
|
33
|
+
console.log('🖼 ctv-run 기본 아이콘을 사용합니다.');
|
|
34
|
+
}
|
|
24
35
|
else {
|
|
25
36
|
const dummyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', 'base64');
|
|
26
37
|
fs_extra_1.default.writeFileSync(targetIconPath, dummyPng);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zsukim/ctv-run",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Smart TV deployment CLI for Vizio, LG webOS, Fire TV, and Samsung Tizen",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ctv",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"node": ">=22"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc",
|
|
29
|
+
"build": "tsc && node -e \"require('fs-extra').copySync('src/assets/icon.png', 'dist/icon.png')\"",
|
|
30
30
|
"watch": "tsc -w",
|
|
31
|
-
"prepublishOnly": "tsc",
|
|
31
|
+
"prepublishOnly": "tsc && node -e \"require('fs-extra').copySync('src/assets/icon.png', 'dist/icon.png')\"",
|
|
32
32
|
"release:patch": "npm version patch && git push && git push --tags",
|
|
33
33
|
"release:minor": "npm version minor && git push && git push --tags",
|
|
34
34
|
"release:major": "npm version major && git push && git push --tags"
|