@zsukim/ctv-run 1.0.0
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 +277 -0
- package/dist/config.js +29 -0
- package/dist/firetv.js +67 -0
- package/dist/index.js +61 -0
- package/dist/platforms/firetv.js +119 -0
- package/dist/platforms/tizen.js +224 -0
- package/dist/platforms/vizio.js +162 -0
- package/dist/platforms/webos.js +150 -0
- package/dist/tizen.js +94 -0
- package/dist/util.js +35 -0
- package/dist/utils/config.js +20 -0
- package/dist/utils/ip.js +14 -0
- package/dist/utils/platform.js +29 -0
- package/dist/vizio.js +161 -0
- package/dist/webos.js +87 -0
- package/package.json +43 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runTizen = runTizen;
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const config_1 = require("../utils/config");
|
|
20
|
+
const ip_1 = require("../utils/ip");
|
|
21
|
+
const platform_1 = require("../utils/platform");
|
|
22
|
+
function runTizen() {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
var _a, _b, _c;
|
|
25
|
+
const config = (0, config_1.loadConfig)();
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const getArg = (0, platform_1.makeArgParser)(args);
|
|
28
|
+
const tvIp = getArg('ip') || ((_a = config.tizen) === null || _a === void 0 ? void 0 : _a.ip);
|
|
29
|
+
if (!tvIp) {
|
|
30
|
+
console.error('\n❌ Tizen TV IP 주소가 필요합니다! (예: --ip=192.168.x.x)');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const targetArg = getArg('target');
|
|
34
|
+
const isLiveMode = !targetArg;
|
|
35
|
+
const appUrl = getArg('url') ||
|
|
36
|
+
`http://${(0, ip_1.getLocalIp)()}:${((_b = config.common) === null || _b === void 0 ? void 0 : _b.devServerPort) || 3000}`;
|
|
37
|
+
const tempAppDir = path_1.default.resolve(process.cwd(), '.ctv-run-tizen');
|
|
38
|
+
if (fs_extra_1.default.existsSync(tempAppDir))
|
|
39
|
+
fs_extra_1.default.removeSync(tempAppDir);
|
|
40
|
+
fs_extra_1.default.ensureDirSync(tempAppDir);
|
|
41
|
+
try {
|
|
42
|
+
console.log(`🚀 [${isLiveMode ? 'Live Mode' : 'Static Mode'}] 준비 중...`);
|
|
43
|
+
// 콘텐츠 준비 및 기존 config.xml 탐색
|
|
44
|
+
if (isLiveMode) {
|
|
45
|
+
// Live Mode: 루트나 public 폴더에서 config.xml이 있는지 찾아서 복사
|
|
46
|
+
const searchPaths = [
|
|
47
|
+
path_1.default.resolve(process.cwd(), 'config.xml'),
|
|
48
|
+
path_1.default.resolve(process.cwd(), 'public', 'config.xml'),
|
|
49
|
+
];
|
|
50
|
+
const foundPath = searchPaths.find((p) => fs_extra_1.default.existsSync(p));
|
|
51
|
+
if (foundPath)
|
|
52
|
+
fs_extra_1.default.copySync(foundPath, path_1.default.join(tempAppDir, 'config.xml'));
|
|
53
|
+
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>`;
|
|
54
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(tempAppDir, 'index.html'), redirectHtml);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Static Mode: 빌드 타겟 폴더 복사
|
|
58
|
+
const distPath = path_1.default.resolve(process.cwd(), targetArg);
|
|
59
|
+
if (!fs_extra_1.default.existsSync(distPath))
|
|
60
|
+
throw new Error(`빌드 폴더 없음: ${distPath}`);
|
|
61
|
+
fs_extra_1.default.copySync(distPath, tempAppDir);
|
|
62
|
+
}
|
|
63
|
+
// 정보 추출 (임시 폴더 우선, 없으면 프로젝트 루트 탐색)
|
|
64
|
+
let xmlInfo = getInfoFromXml(tempAppDir);
|
|
65
|
+
if (!xmlInfo)
|
|
66
|
+
xmlInfo = getInfoFromXml(process.cwd());
|
|
67
|
+
const appId = getArg('appId') ||
|
|
68
|
+
(xmlInfo === null || xmlInfo === void 0 ? void 0 : xmlInfo.appId) ||
|
|
69
|
+
((_c = config.tizen) === null || _c === void 0 ? void 0 : _c.appId) ||
|
|
70
|
+
'A123456789.CtvRunApp';
|
|
71
|
+
const safeAppName = ((xmlInfo === null || xmlInfo === void 0 ? void 0 : xmlInfo.appName) || 'CtvRunApp').replace(/\s+/g, '');
|
|
72
|
+
// config.xml 없으면 생성
|
|
73
|
+
yield ensureTizenFiles(appId, tempAppDir, safeAppName);
|
|
74
|
+
// Tizen 인증서 프로필 확인/생성/활성화
|
|
75
|
+
console.log('\n🎫 Tizen 인증서 프로필 확인 중...');
|
|
76
|
+
const getActive = () => {
|
|
77
|
+
try {
|
|
78
|
+
const list = (0, child_process_1.execSync)('tizen security-profiles list', {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
});
|
|
81
|
+
const match = list.match(/Active Profile\s*:\s*(\S+)/);
|
|
82
|
+
return match ? match[1] : '';
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
let activeProfile = getActive();
|
|
89
|
+
if (!activeProfile) {
|
|
90
|
+
console.log('⚠️ 활성화된 프로필이 없습니다. 앱 전용 인증서를 세팅합니다.');
|
|
91
|
+
const askQuestion = (query) => {
|
|
92
|
+
const buffer = Buffer.alloc(1024);
|
|
93
|
+
process.stdout.write(query);
|
|
94
|
+
const bytesRead = fs_extra_1.default.readSync(0, buffer, 0, 1024, null);
|
|
95
|
+
return buffer.toString('utf8', 0, bytesRead).trim();
|
|
96
|
+
};
|
|
97
|
+
const certFileName = 'ctv-run-cert';
|
|
98
|
+
const profileName = 'ctv-run-profile';
|
|
99
|
+
const homeDir = process.env.HOME || '';
|
|
100
|
+
const defaultCertDir = path_1.default.join(homeDir, 'tizen-studio-data.1/keystore/author');
|
|
101
|
+
const p12Path = path_1.default.join(defaultCertDir, `${certFileName}.p12`);
|
|
102
|
+
let p12Pwd = '';
|
|
103
|
+
if (fs_extra_1.default.existsSync(p12Path)) {
|
|
104
|
+
console.log(`✨ 기존 인증서 발견: ${p12Path}`);
|
|
105
|
+
p12Pwd = askQuestion('🔑 인증서 비밀번호를 입력하세요: ');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
p12Pwd = askQuestion('🔑 새로 만들 인증서 비밀번호(4자 이상): ');
|
|
109
|
+
(0, child_process_1.execSync)(`tizen certificate -a "${certFileName}" -p "${p12Pwd}" -f "${certFileName}"`, { stdio: 'inherit' });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
(0, child_process_1.execSync)(`tizen security-profiles remove -n "${profileName}"`, {
|
|
113
|
+
stdio: 'ignore',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (e) { }
|
|
117
|
+
(0, child_process_1.execSync)(`tizen security-profiles add -n "${profileName}" -a "${p12Path}" -p "${p12Pwd}"`, { stdio: 'inherit' });
|
|
118
|
+
(0, child_process_1.execSync)(`tizen security-profiles set-active -n "${profileName}"`, {
|
|
119
|
+
stdio: 'inherit',
|
|
120
|
+
});
|
|
121
|
+
activeProfile = getActive();
|
|
122
|
+
}
|
|
123
|
+
// 패키징
|
|
124
|
+
console.log(`\n🔨 [${activeProfile}] 프로필로 패키징 중...`);
|
|
125
|
+
(0, child_process_1.execSync)(`tizen package -t wgt -s "${activeProfile}" -o "${tempAppDir}" -- "${tempAppDir}"`, { stdio: 'inherit' });
|
|
126
|
+
const files = fs_extra_1.default.readdirSync(tempAppDir);
|
|
127
|
+
const generatedWgt = files.find((f) => f.endsWith('.wgt'));
|
|
128
|
+
if (!generatedWgt)
|
|
129
|
+
throw new Error('.wgt 패키지 생성 실패!');
|
|
130
|
+
const wgtPath = path_1.default.join(tempAppDir, generatedWgt);
|
|
131
|
+
// TV 연결 및 설치
|
|
132
|
+
console.log(`\n📡 TV 연결 시도: ${tvIp}`);
|
|
133
|
+
try {
|
|
134
|
+
(0, child_process_1.execSync)(`sdb connect ${tvIp}`, { stdio: 'ignore' });
|
|
135
|
+
}
|
|
136
|
+
catch (e) { }
|
|
137
|
+
console.log(`📦 앱 설치 중: ${generatedWgt}`);
|
|
138
|
+
try {
|
|
139
|
+
(0, child_process_1.execSync)(`tizen install -n "${wgtPath}"`, { stdio: 'inherit' });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
console.log('\n⚠️ 설치 실패! (인증서 불일치 가능성이 있습니다.)');
|
|
143
|
+
const askQuestion = (query) => {
|
|
144
|
+
const buffer = Buffer.alloc(1024);
|
|
145
|
+
process.stdout.write(query);
|
|
146
|
+
const bytesRead = fs_extra_1.default.readSync(0, buffer, 0, 1024, null);
|
|
147
|
+
return buffer.toString('utf8', 0, bytesRead).trim().toLowerCase();
|
|
148
|
+
};
|
|
149
|
+
const answer = askQuestion('❓ 기존 앱을 삭제하고 다시 설치할까요? (Y/n): ');
|
|
150
|
+
if (answer === 'Y') {
|
|
151
|
+
console.log(`🗑️ 기존 앱 삭제 중: ${appId}`);
|
|
152
|
+
(0, child_process_1.execSync)(`tizen uninstall -p ${appId}`, { stdio: 'inherit' });
|
|
153
|
+
console.log('✅ 삭제 완료. 재설치 시도...');
|
|
154
|
+
(0, child_process_1.execSync)(`tizen install -n "${wgtPath}"`, { stdio: 'inherit' });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log('🚫 설치 중단.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 앱 실행
|
|
162
|
+
console.log(`🚀 앱 실행 중: ${appId}`);
|
|
163
|
+
(0, child_process_1.execSync)(`tizen run -p ${appId}`, { stdio: 'inherit' });
|
|
164
|
+
console.log(`\n✅ 모든 과정 완료!`);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.error(`\n❌ 오류 발생: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (fs_extra_1.default.existsSync(tempAppDir))
|
|
171
|
+
fs_extra_1.default.removeSync(tempAppDir);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function getInfoFromXml(dir) {
|
|
176
|
+
const xmlPath = path_1.default.join(dir, 'config.xml');
|
|
177
|
+
if (fs_extra_1.default.existsSync(xmlPath)) {
|
|
178
|
+
const content = fs_extra_1.default.readFileSync(xmlPath, 'utf-8');
|
|
179
|
+
// id 찾기
|
|
180
|
+
const tizenIdMatch = content.match(/<tizen:application[^>]*id="([^"]+)"/);
|
|
181
|
+
const widgetIdMatch = content.match(/<widget[^>]*id="([^"]+)"/);
|
|
182
|
+
const pkgMatch = content.match(/package="([^"]+)"/);
|
|
183
|
+
const nameMatch = content.match(/<name>([^<]+)<\/name>/);
|
|
184
|
+
return {
|
|
185
|
+
// tizen:application id를 우선하고, 없으면 widget id를 봅니다.
|
|
186
|
+
appId: tizenIdMatch
|
|
187
|
+
? tizenIdMatch[1]
|
|
188
|
+
: widgetIdMatch
|
|
189
|
+
? widgetIdMatch[1]
|
|
190
|
+
: null,
|
|
191
|
+
pkgId: pkgMatch ? pkgMatch[1] : null,
|
|
192
|
+
appName: nameMatch ? nameMatch[1] : null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function ensureTizenFiles(appId, tempDir, appName) {
|
|
198
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
199
|
+
const configXmlPath = path_1.default.join(tempDir, 'config.xml');
|
|
200
|
+
// 기존에 꼬여있을 수 있는 config.xml을 삭제하고 새로 만듭니다.
|
|
201
|
+
if (fs_extra_1.default.existsSync(configXmlPath)) {
|
|
202
|
+
fs_extra_1.default.removeSync(configXmlPath);
|
|
203
|
+
}
|
|
204
|
+
console.log(`📝 config.xml 생성 중... (Name: ${appName}, ID: ${appId})`);
|
|
205
|
+
// appId 추출
|
|
206
|
+
const packageId = appId.split('.')[0] || 'TizenApp';
|
|
207
|
+
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
208
|
+
<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">
|
|
209
|
+
<tizen:application id="${appId}" package="${packageId}" required_version="2.3"/>
|
|
210
|
+
<tizen:profile name="tv-samsung"/>
|
|
211
|
+
<content src="index.html"/>
|
|
212
|
+
<name>${appName}</name>
|
|
213
|
+
<icon src="icon.png"/>
|
|
214
|
+
<access origin="*" subdomains="true"/>
|
|
215
|
+
<tizen:allow-navigation>*</tizen:allow-navigation>
|
|
216
|
+
<tizen:privilege name="http://tizen.org/privilege/internet"/>
|
|
217
|
+
<tizen:privilege name="http://developer.samsung.com/privilege/network.public"/>
|
|
218
|
+
<tizen:privilege name="http://tizen.org/privilege/tv.inputdevice" />
|
|
219
|
+
<tizen:setting screen-orientation="landscape" context-menu="enable" background-support="disable" encryption="disable" install-location="auto" hwkey-event="enable"/>
|
|
220
|
+
</widget>`;
|
|
221
|
+
fs_extra_1.default.writeFileSync(configXmlPath, xmlContent, 'utf8');
|
|
222
|
+
(0, platform_1.resolveIcon)(tempDir);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.runVizio = runVizio;
|
|
46
|
+
const playwright_1 = require("playwright");
|
|
47
|
+
const readline = __importStar(require("readline/promises"));
|
|
48
|
+
const config_1 = require("../utils/config");
|
|
49
|
+
const ip_1 = require("../utils/ip");
|
|
50
|
+
const platform_1 = require("../utils/platform");
|
|
51
|
+
function runVizio() {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
var _a, _b, _c, _d;
|
|
54
|
+
const rl = readline.createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout,
|
|
57
|
+
});
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const config = (0, config_1.loadConfig)();
|
|
60
|
+
const getArg = (0, platform_1.makeArgParser)(args);
|
|
61
|
+
const tvIp = getArg('ip') || ((_a = config.vizio) === null || _a === void 0 ? void 0 : _a.ip);
|
|
62
|
+
if (!tvIp) {
|
|
63
|
+
console.error(`\n❌ Vizio TV의 IP 주소가 없습니다!`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const port = getArg('port') || ((_b = config.vizio) === null || _b === void 0 ? void 0 : _b.port) || ((_c = config.common) === null || _c === void 0 ? void 0 : _c.devServerPort) || 3000;
|
|
67
|
+
const appUrl = getArg('url') || ((_d = config.vizio) === null || _d === void 0 ? void 0 : _d.url) || `http://${(0, ip_1.getLocalIp)()}:${port}`;
|
|
68
|
+
const launcherUrl = 'https://vizio-pm.s3-us-west-1.amazonaws.com/conjure-launcher.html';
|
|
69
|
+
console.log(`-----------------------------------------`);
|
|
70
|
+
console.log(`📡 Vizio TV IP : ${tvIp}`);
|
|
71
|
+
console.log(`🚀 App URL : ${appUrl}`);
|
|
72
|
+
console.log(`-----------------------------------------`);
|
|
73
|
+
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
|
|
74
|
+
let browser;
|
|
75
|
+
try {
|
|
76
|
+
browser = yield playwright_1.chromium.launch({
|
|
77
|
+
headless: false,
|
|
78
|
+
args: [
|
|
79
|
+
'--disable-web-security',
|
|
80
|
+
'--disable-features=BlockInsecurePrivateNetworkRequests',
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err.message.includes('executable') ||
|
|
86
|
+
err.message.includes('not installed')) {
|
|
87
|
+
console.error('\n❌ Playwright 브라우저(Chromium)가 설치되지 않았습니다.');
|
|
88
|
+
console.error('👉 아래 명령어를 실행하여 브라우저를 설치해주세요:');
|
|
89
|
+
console.error(' npx playwright install chromium\n');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error('\n❌ 브라우저 실행 중 알 수 없는 에러가 발생했습니다:');
|
|
93
|
+
console.error(` ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const context = yield browser.newContext({ ignoreHTTPSErrors: true });
|
|
98
|
+
const page = yield context.newPage();
|
|
99
|
+
try {
|
|
100
|
+
console.log(`🌐 런처 접속 중: ${launcherUrl}`);
|
|
101
|
+
yield page.goto(launcherUrl);
|
|
102
|
+
console.log(`⌨️ TV IP 입력 중: ${tvIp}`);
|
|
103
|
+
yield page.waitForSelector('input[placeholder*="IP"]');
|
|
104
|
+
yield page.fill('input[placeholder*="IP"]', tvIp);
|
|
105
|
+
console.log('🔘 Start Pairing 클릭');
|
|
106
|
+
yield page.locator('input[value="Start Pairing"]').click();
|
|
107
|
+
const sslLink = page.locator('a', { hasText: 'Start SSL process' });
|
|
108
|
+
if (yield sslLink.isVisible({ timeout: 5000 })) {
|
|
109
|
+
console.log('⏳ SSL 인증 페이지 자동 진행 시도...');
|
|
110
|
+
const [sslPage] = yield Promise.all([
|
|
111
|
+
context.waitForEvent('page'),
|
|
112
|
+
sslLink.click(),
|
|
113
|
+
]);
|
|
114
|
+
if (sslPage) {
|
|
115
|
+
yield sslPage.waitForLoadState('domcontentloaded');
|
|
116
|
+
try {
|
|
117
|
+
yield sslPage.locator('#details-button').click({ timeout: 5000 });
|
|
118
|
+
yield sslPage.locator('#proceed-link').click({ timeout: 5000 });
|
|
119
|
+
console.log('✅ SSL 인증 수락 완료.');
|
|
120
|
+
yield sslPage.waitForTimeout(1000);
|
|
121
|
+
yield sslPage.close();
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
console.log('ℹ️ SSL 경고창이 이미 승인되었거나 뜨지 않았습니다.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const continueBtn = page.locator('a', { hasText: 'Continue' });
|
|
129
|
+
if (yield continueBtn.isVisible()) {
|
|
130
|
+
yield continueBtn.click();
|
|
131
|
+
}
|
|
132
|
+
console.log('----------------------------------------------------');
|
|
133
|
+
console.log('📺 TV 화면의 PIN 4자리를 입력하세요.');
|
|
134
|
+
const pin = yield rl.question('PIN: ');
|
|
135
|
+
console.log('----------------------------------------------------');
|
|
136
|
+
yield page.waitForSelector('input[placeholder*="PIN"]');
|
|
137
|
+
yield page.fill('input[placeholder*="PIN"]', pin);
|
|
138
|
+
console.log('🔘 Pair 버튼 클릭');
|
|
139
|
+
yield page.locator('input[value="Pair"]').click();
|
|
140
|
+
console.log(`🔗 App URL 주입 중: ${appUrl}`);
|
|
141
|
+
const urlInput = page.locator('#app-url');
|
|
142
|
+
yield urlInput.waitFor({ state: 'visible', timeout: 30000 });
|
|
143
|
+
yield urlInput.click();
|
|
144
|
+
yield urlInput.fill(appUrl);
|
|
145
|
+
console.log(`✅ URL 주입 완료: ${appUrl}`);
|
|
146
|
+
console.log('🚀 Launch 버튼 클릭!');
|
|
147
|
+
yield page.locator('#launch-btn').click();
|
|
148
|
+
console.log('✅ 모든 명령이 성공적으로 전달되었습니다!');
|
|
149
|
+
yield page.waitForTimeout(2000);
|
|
150
|
+
yield browser.close();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error('\n❌ 실행 중 에러 발생:', err.message);
|
|
155
|
+
if (browser)
|
|
156
|
+
yield browser.close();
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
rl.close();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runWebOS = runWebOS;
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const config_1 = require("../utils/config");
|
|
20
|
+
const ip_1 = require("../utils/ip");
|
|
21
|
+
const platform_1 = require("../utils/platform");
|
|
22
|
+
function runWebOS() {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
var _a, _b, _c;
|
|
25
|
+
const config = (0, config_1.loadConfig)();
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const getArg = (0, platform_1.makeArgParser)(args);
|
|
28
|
+
// 기본 설정값 로드
|
|
29
|
+
const deviceName = getArg('device') || ((_a = config.webos) === null || _a === void 0 ? void 0 : _a.deviceName);
|
|
30
|
+
const appId = getArg('appId') || ((_b = config.webos) === null || _b === void 0 ? void 0 : _b.appId) || 'com.ctvrun.app';
|
|
31
|
+
const targetArg = getArg('target');
|
|
32
|
+
const appUrl = getArg('url') ||
|
|
33
|
+
`http://${(0, ip_1.getLocalIp)()}:${((_c = config.common) === null || _c === void 0 ? void 0 : _c.devServerPort) || 3000}`;
|
|
34
|
+
// 등록된 기기가 없는 경우
|
|
35
|
+
if (!deviceName) {
|
|
36
|
+
console.log('\n📺 연결된 WebOS TV 정보가 없습니다.');
|
|
37
|
+
try {
|
|
38
|
+
const deviceList = (0, child_process_1.execSync)('ares-setup-device --list', {
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
});
|
|
41
|
+
// 등록된 기기가 아예 없거나 헤더만 있는 경우
|
|
42
|
+
if (!deviceList.includes('ssh')) {
|
|
43
|
+
console.log('✨ 등록된 기기가 없습니다. 기기 등록(add)을 시작합니다...');
|
|
44
|
+
console.log('-----------------------------------------');
|
|
45
|
+
// 사용자가 직접 입력할 수 있도록 제어권을 넘김
|
|
46
|
+
(0, child_process_1.execSync)('ares-setup-device', { stdio: 'inherit' });
|
|
47
|
+
console.log('-----------------------------------------');
|
|
48
|
+
console.log('\n✅ 기기 등록 완료! 다시 "ctv-run webos"를 실행해주세요.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// 기기는 있는데 이름을 지정 안 한 경우
|
|
53
|
+
console.log('🔍 현재 등록된 기기 목록입니다:');
|
|
54
|
+
console.log(deviceList);
|
|
55
|
+
console.log('💡 실행 방법: ctv-run webos --device=기기이름');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error('❌ ares-cli(WebOS SDK)가 설치되어 있지 않습니다.');
|
|
61
|
+
console.log('👉 https://webostv.developer.lge.com 에서 SDK를 먼저 설치해주세요.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const tempAppDir = path_1.default.resolve(process.cwd(), '.ctv-run-webos');
|
|
66
|
+
try {
|
|
67
|
+
// 빌드/실행을 위한 임시 환경 준비
|
|
68
|
+
if (fs_extra_1.default.existsSync(tempAppDir))
|
|
69
|
+
fs_extra_1.default.removeSync(tempAppDir);
|
|
70
|
+
fs_extra_1.default.ensureDirSync(tempAppDir);
|
|
71
|
+
// appinfo.json 생성
|
|
72
|
+
yield ensureWebosAppInfo(appId, tempAppDir);
|
|
73
|
+
// 콘텐츠 모드 설정
|
|
74
|
+
if (targetArg) {
|
|
75
|
+
const distPath = path_1.default.resolve(process.cwd(), targetArg);
|
|
76
|
+
if (!fs_extra_1.default.existsSync(distPath))
|
|
77
|
+
throw new Error(`폴더를 찾을 수 없습니다: ${distPath}`);
|
|
78
|
+
console.log(`📦 [Static Mode] Copying from ${distPath}...`);
|
|
79
|
+
fs_extra_1.default.copySync(distPath, tempAppDir, { overwrite: true });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`🌐 [Live Mode] Connecting to ${appUrl}...`);
|
|
83
|
+
const redirectHtml = `<!DOCTYPE html>
|
|
84
|
+
<html>
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="utf-8">
|
|
87
|
+
</head>
|
|
88
|
+
<body style="background:#000;">
|
|
89
|
+
<script type="text/javascript">
|
|
90
|
+
window.onerror = function() { return true; };
|
|
91
|
+
setTimeout(function() {
|
|
92
|
+
window.location.replace("${appUrl}");
|
|
93
|
+
}, 200);
|
|
94
|
+
</script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
97
|
+
`;
|
|
98
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(tempAppDir, 'index.html'), redirectHtml);
|
|
99
|
+
}
|
|
100
|
+
// 아이콘
|
|
101
|
+
(0, platform_1.resolveIcon)(tempAppDir);
|
|
102
|
+
// 패키징
|
|
103
|
+
console.log(`🔨 Packaging [${appId}]...`);
|
|
104
|
+
(0, child_process_1.execSync)(`ares-package ${tempAppDir} -o .`, { stdio: 'inherit' });
|
|
105
|
+
const ipkFile = fs_extra_1.default
|
|
106
|
+
.readdirSync(process.cwd())
|
|
107
|
+
.find((f) => f.startsWith(appId) && f.endsWith('.ipk'));
|
|
108
|
+
if (ipkFile) {
|
|
109
|
+
// 설치
|
|
110
|
+
console.log(`🚀 Installing to ${deviceName}...`);
|
|
111
|
+
(0, child_process_1.execSync)(`ares-install -d ${deviceName} ${ipkFile}`, {
|
|
112
|
+
stdio: 'inherit',
|
|
113
|
+
});
|
|
114
|
+
// 런치
|
|
115
|
+
console.log(`▶️ Launching App...`);
|
|
116
|
+
(0, child_process_1.execSync)(`ares-launch -d ${deviceName} ${appId}`, { stdio: 'inherit' });
|
|
117
|
+
// 파일 정리
|
|
118
|
+
fs_extra_1.default.removeSync(path_1.default.resolve(process.cwd(), ipkFile));
|
|
119
|
+
fs_extra_1.default.removeSync(tempAppDir);
|
|
120
|
+
console.log(`\n✅ webOS 실행 성공!`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error(`\n❌ Error: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// appinfo.json이 없으면 자동 생성
|
|
129
|
+
function ensureWebosAppInfo(appId, tempDir) {
|
|
130
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
+
const appInfoPath = path_1.default.join(tempDir, 'appinfo.json');
|
|
132
|
+
const rootAppInfo = path_1.default.resolve(process.cwd(), 'appinfo.json');
|
|
133
|
+
if (fs_extra_1.default.existsSync(rootAppInfo)) {
|
|
134
|
+
fs_extra_1.default.copySync(rootAppInfo, appInfoPath);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const defaultAppInfo = {
|
|
138
|
+
id: appId,
|
|
139
|
+
version: '1.0.0',
|
|
140
|
+
vendor: 'CtvRun',
|
|
141
|
+
type: 'web',
|
|
142
|
+
main: 'index.html',
|
|
143
|
+
title: 'CTV-Run-App',
|
|
144
|
+
icon: 'icon.png',
|
|
145
|
+
uiRevision: 2,
|
|
146
|
+
};
|
|
147
|
+
fs_extra_1.default.writeJsonSync(appInfoPath, defaultAppInfo, { spaces: 2 });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
package/dist/tizen.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runTizen = runTizen;
|
|
16
|
+
const wits_1 = require("@tizentv/wits");
|
|
17
|
+
const util_1 = require("./util");
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
function runTizen() {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
24
|
+
const config = (0, util_1.loadConfig)();
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
|
|
27
|
+
const tvIp = getArg('ip') || ((_a = config.tizen) === null || _a === void 0 ? void 0 : _a.ip);
|
|
28
|
+
const appId = getArg('appId') || ((_b = config.tizen) === null || _b === void 0 ? void 0 : _b.appId);
|
|
29
|
+
const profileName = getArg('profile') || ((_c = config.tizen) === null || _c === void 0 ? void 0 : _c.profileName);
|
|
30
|
+
const witsConfigPath = path_1.default.join(process.cwd(), '.witsconfig.json');
|
|
31
|
+
// [수정 포인트 1] 기존 설정을 먼저 읽어와야 함
|
|
32
|
+
let existingWitsConfig = {};
|
|
33
|
+
if (fs_extra_1.default.existsSync(witsConfigPath)) {
|
|
34
|
+
try {
|
|
35
|
+
existingWitsConfig = fs_extra_1.default.readJsonSync(witsConfigPath);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
existingWitsConfig = {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 1. 인증서가 없는 경우 (인자, config, 기존 파일 모두 뒤져도 없을 때)
|
|
42
|
+
const finalProfileName = profileName || ((_d = existingWitsConfig.profileInfo) === null || _d === void 0 ? void 0 : _d.name);
|
|
43
|
+
if (!finalProfileName && !fs_extra_1.default.existsSync(witsConfigPath)) {
|
|
44
|
+
console.log("🎫 Tizen 인증서 설정이 없습니다. 'wits -c'를 실행합니다...");
|
|
45
|
+
try {
|
|
46
|
+
(0, child_process_1.execSync)('npx wits -c', { stdio: 'inherit' });
|
|
47
|
+
console.log("\n✅ 인증서 생성이 완료되었습니다. 다시 실행해주세요.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
console.error("❌ 인증서 생성 실패");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 2. 필수 값 체크
|
|
56
|
+
if (!tvIp || !appId) {
|
|
57
|
+
console.error("\n❌ Tizen 필수 설정이 누락되었습니다!");
|
|
58
|
+
console.log(`\n[해결 방법]`);
|
|
59
|
+
console.log(`1. ctv.config.json에 'tizen.ip'와 'tizen.appId'를 설정하세요.`);
|
|
60
|
+
console.log(`2. 또는 실행 시 인자를 추가하세요: ctv-run tizen --ip=192.168.x.x --appId=YOUR_APP_ID`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// [수정 포인트 2] 조립 시 existingWitsConfig를 참조하도록 수정
|
|
64
|
+
const finalConfig = {
|
|
65
|
+
connectionInfo: {
|
|
66
|
+
deviceIp: tvIp,
|
|
67
|
+
hostIp: (0, util_1.getLocalIp)(),
|
|
68
|
+
width: ((_e = existingWitsConfig.connectionInfo) === null || _e === void 0 ? void 0 : _e.width) || "1920"
|
|
69
|
+
},
|
|
70
|
+
profileInfo: {
|
|
71
|
+
name: finalProfileName,
|
|
72
|
+
path: ((_f = config.tizen) === null || _f === void 0 ? void 0 : _f.profilePath) || ((_g = existingWitsConfig.profileInfo) === null || _g === void 0 ? void 0 : _g.path) || ""
|
|
73
|
+
},
|
|
74
|
+
appInfo: {
|
|
75
|
+
appId: appId,
|
|
76
|
+
baseAppPath: "index.html",
|
|
77
|
+
appWorkspace: process.cwd()
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
console.log("🛠️ Updating .witsconfig.json...");
|
|
82
|
+
fs_extra_1.default.writeFileSync(witsConfigPath, JSON.stringify(finalConfig, null, 2));
|
|
83
|
+
const wits = new wits_1.Wits();
|
|
84
|
+
console.log("🚀 Starting Wits (Tizen Live Development)...");
|
|
85
|
+
yield wits.init();
|
|
86
|
+
yield wits.start();
|
|
87
|
+
yield wits.watch();
|
|
88
|
+
console.log("✅ Tizen App is running!");
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error(`❌ Tizen Error: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
exports.getLocalIp = getLocalIp;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = require("os");
|
|
11
|
+
/**
|
|
12
|
+
* 설정 파일을 로드하는 헬퍼 함수
|
|
13
|
+
*/
|
|
14
|
+
function loadConfig() {
|
|
15
|
+
const configPath = path_1.default.resolve(process.cwd(), 'ctv.config.json');
|
|
16
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.warn('⚠️ ctv.config.json 형식이 올바르지 않습니다. 기본값을 사용합니다.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
function getLocalIp() {
|
|
27
|
+
const nets = (0, os_1.networkInterfaces)();
|
|
28
|
+
for (const name of Object.keys(nets)) {
|
|
29
|
+
for (const net of nets[name]) {
|
|
30
|
+
if (net.family === 'IPv4' && !net.internal)
|
|
31
|
+
return net.address;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return 'localhost';
|
|
35
|
+
}
|