@zsukim/ctv-run 1.0.6 → 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 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/index.js CHANGED
File without changes
@@ -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 tizenBin = getArg('tizenPath') || ((_b = config.tizen) === null || _b === void 0 ? void 0 : _b.tizenPath) || 'tizen';
34
- const sdbBin = getArg('sdbPath') || ((_c = config.tizen) === null || _c === void 0 ? void 0 : _c.sdbPath) || 'sdb';
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') || ((_d = config.tizen) === null || _d === void 0 ? void 0 : _d.port) || ((_e = config.common) === null || _e === void 0 ? void 0 : _e.devServerPort) || 3000;
38
- const appUrl = getArg('url') ||
39
- ((_f = config.tizen) === null || _f === void 0 ? void 0 : _f.url) ||
40
- `http://${(0, ip_1.getLocalIp)()}:${port}`;
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: 루트나 public 폴더에서 config.xml이 있는지 찾아서 복사
50
- const searchPaths = [
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 getActive = () => {
88
+ const profilesXmlPath = path_1.default.join(process.env.HOME || '', 'tizen-studio-data/profile/profiles.xml');
89
+ const readProfilesXml = () => {
81
90
  try {
82
- const list = (0, child_process_1.execSync)(`${tizenBin} security-profiles list`, {
83
- encoding: 'utf8',
84
- });
85
- const match = list.match(/Active Profile\s*:\s*(\S+)/);
86
- return match ? match[1] : '';
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
- let activeProfile = getActive();
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
- const certFileName = 'ctv-run-cert';
102
- const profileName = 'ctv-run-profile';
103
- const homeDir = process.env.HOME || '';
104
- const defaultCertDir = path_1.default.join(homeDir, 'tizen-studio-data.1/keystore/author');
105
- const p12Path = path_1.default.join(defaultCertDir, `${certFileName}.p12`);
106
- let p12Pwd = '';
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 === 'Y') {
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 (fs_extra_1.default.existsSync(tempAppDir))
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(appId, tempDir, appName) {
202
- return __awaiter(this, void 0, void 0, function* () {
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
- // 기존에 꼬여있을 수 있는 config.xml을 삭제하고 새로 만듭니다.
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.3"/>
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>${appName}</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
  }
@@ -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 sourceIconPath = path_1.default.resolve(process.cwd(), 'src', 'assets', 'icon.png');
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
- if (fs_extra_1.default.existsSync(sourceIconPath)) {
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.6",
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",
@@ -21,13 +21,17 @@
21
21
  "dist",
22
22
  "README.md"
23
23
  ],
24
+ "packageManager": "pnpm@10.2.0",
24
25
  "engines": {
25
- "node": ">=18"
26
+ "node": ">=22"
26
27
  },
27
28
  "scripts": {
28
- "build": "tsc",
29
+ "build": "tsc && node -e \"require('fs-extra').copySync('src/assets/icon.png', 'dist/icon.png')\"",
29
30
  "watch": "tsc -w",
30
- "prepublishOnly": "tsc"
31
+ "prepublishOnly": "tsc && node -e \"require('fs-extra').copySync('src/assets/icon.png', 'dist/icon.png')\"",
32
+ "release:patch": "npm version patch && git push && git push --tags",
33
+ "release:minor": "npm version minor && git push && git push --tags",
34
+ "release:major": "npm version major && git push && git push --tags"
31
35
  },
32
36
  "dependencies": {
33
37
  "fs-extra": "^11.3.4",
package/dist/config.js DELETED
@@ -1,29 +0,0 @@
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.CONFIG = void 0;
7
- const dotenv_1 = __importDefault(require("dotenv"));
8
- dotenv_1.default.config();
9
- // export function getTargetIp(platform: string) {
10
- // const configPath = path.join(process.cwd(), 'ctv.config.json');
11
- //
12
- // // 1. 설정 파일이 있으면 읽어오기
13
- // if (fs.existsSync(configPath)) {
14
- // const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
15
- // if (config.platforms?.[platform]?.ip) {
16
- // return config.platforms[platform].ip;
17
- // }
18
- // }
19
- //
20
- // // 2. 설정 파일이 없으면 환경변수나 기본값 사용 (테스트용)
21
- // return process.env.VIZIO_TV_IP || '0.0.0.0';
22
- // }
23
- exports.CONFIG = {
24
- DIST_PATH: './dist',
25
- PORT: 3000,
26
- // .env 파일에 TV_IP=192.168.x.x 형태로 저장하세요
27
- VIZIO_TV_IP: process.env.VIZIO_TV_IP || '0.0.0.0',
28
- LAUNCHER_URL: 'https://vizio-pm.s3-us-west-1.amazonaws.com/conjure-launcher.html',
29
- };
package/dist/firetv.js DELETED
@@ -1,67 +0,0 @@
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
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.runFireTv = runFireTv;
13
- const child_process_1 = require("child_process");
14
- const util_1 = require("./util");
15
- function runFireTv() {
16
- return __awaiter(this, void 0, void 0, function* () {
17
- var _a, _b, _c;
18
- const args = process.argv.slice(2);
19
- const config = (0, util_1.loadConfig)();
20
- const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
21
- const tvIp = getArg('ip') || ((_a = config.firetv) === null || _a === void 0 ? void 0 : _a.ip);
22
- const pkg = getArg('package') || ((_b = config.firetv) === null || _b === void 0 ? void 0 : _b.package);
23
- const targetArg = getArg('target'); // 'dist' 등이 들어오면 번들 모드로 간주
24
- const activity = getArg('activity') || ((_c = config.firetv) === null || _c === void 0 ? void 0 : _c.activity) || '.MainActivity';
25
- const appUrl = getArg('url') || `http://${(0, util_1.getLocalIp)()}:5173`;
26
- if (!tvIp || !pkg) {
27
- console.error(`\n❌ Fire TV 필수 설정(ip, package)이 누락되었습니다!`);
28
- return;
29
- }
30
- console.log(`-----------------------------------------`);
31
- console.log(`📡 Target TV IP : ${tvIp}`);
32
- console.log(`📦 Package Name : ${pkg}`);
33
- if (targetArg) {
34
- console.log(`📦 Mode : 🏠 Bundle Mode (Local App)`);
35
- }
36
- else {
37
- console.log(`📦 Mode : 🌐 Live Mode (Remote URL)`);
38
- console.log(`🚀 App URL : ${appUrl}`);
39
- }
40
- console.log(`-----------------------------------------`);
41
- try {
42
- // 1. ADB 연결
43
- console.log(`🔗 ADB Connecting to ${tvIp}...`);
44
- (0, child_process_1.execSync)(`adb connect ${tvIp}:5555`, { stdio: 'pipe' });
45
- // 2. 기존 앱 종료
46
- console.log('🔄 Force stopping existing process...');
47
- (0, child_process_1.execSync)(`adb -s ${tvIp}:5555 shell am force-stop ${pkg}`);
48
- // 3. 실행 커맨드 조립
49
- const fullActivityPath = activity.startsWith('.') ? `${pkg}/${activity}` : `${pkg}/${pkg}${activity}`;
50
- let launchCmd = `adb -s ${tvIp}:5555 shell am start -n ${fullActivityPath}`;
51
- /**
52
- * [핵심 로직]
53
- * target 인자가 없으면(Live 모드), TARGET_URL 에너테이션을 추가해서 보냅니다.
54
- * target 인자가 있으면(Bundle 모드), 그냥 앱만 실행합니다.
55
- */
56
- if (!targetArg) {
57
- launchCmd += ` --es "TARGET_URL" "${appUrl}"`;
58
- }
59
- console.log('📱 Launching App...');
60
- (0, child_process_1.execSync)(launchCmd, { stdio: 'inherit' });
61
- console.log(`\n✅ Success! Fire TV launch complete.`);
62
- }
63
- catch (err) {
64
- console.error(`\n❌ Error: ${err.message}`);
65
- }
66
- });
67
- }
package/dist/tizen.js DELETED
@@ -1,94 +0,0 @@
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 DELETED
@@ -1,35 +0,0 @@
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
- }
package/dist/vizio.js DELETED
@@ -1,161 +0,0 @@
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
- var __importDefault = (this && this.__importDefault) || function (mod) {
45
- return (mod && mod.__esModule) ? mod : { "default": mod };
46
- };
47
- Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.runVizio = runVizio;
49
- const playwright_1 = require("playwright");
50
- const readline = __importStar(require("readline/promises"));
51
- const path_1 = __importDefault(require("path"));
52
- const http_1 = __importDefault(require("http"));
53
- const util_1 = require("./util");
54
- const serveHandler = require('serve-handler');
55
- function runVizio() {
56
- return __awaiter(this, void 0, void 0, function* () {
57
- var _a, _b;
58
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
59
- const args = process.argv.slice(2);
60
- const config = (0, util_1.loadConfig)();
61
- const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
62
- const tvIp = getArg('ip') || ((_a = config.vizio) === null || _a === void 0 ? void 0 : _a.ip);
63
- if (!tvIp) {
64
- console.error(`\n❌ Vizio TV의 IP 주소가 없습니다!`);
65
- return;
66
- }
67
- const defaultPort = Number(getArg('port')) || ((_b = config.vizio) === null || _b === void 0 ? void 0 : _b.port) || 3300;
68
- const launcherUrl = 'https://vizio-pm.s3-us-west-1.amazonaws.com/conjure-launcher.html';
69
- const targetArg = getArg('target');
70
- const urlArg = getArg('url');
71
- let appUrl = '';
72
- if (urlArg) {
73
- appUrl = urlArg;
74
- }
75
- else if (targetArg) {
76
- const distPath = path_1.default.resolve(process.cwd(), targetArg);
77
- const server = http_1.default.createServer((req, res) => serveHandler(req, res, { public: distPath }));
78
- server.listen(3000, '0.0.0.0');
79
- appUrl = `http://${(0, util_1.getLocalIp)()}:3000`;
80
- }
81
- else {
82
- appUrl = `http://${(0, util_1.getLocalIp)()}.nip.io:${defaultPort}`;
83
- }
84
- console.log(`-----------------------------------------`);
85
- console.log(`📡 Vizio TV IP : ${tvIp}`);
86
- console.log(`🚀 App URL : ${appUrl}`);
87
- console.log(`-----------------------------------------`);
88
- // [개선] 로컬 네트워크 권한 팝업을 차단하기 위한 브라우저 실행 인자 설정
89
- const browser = yield playwright_1.chromium.launch({
90
- headless: false,
91
- args: [
92
- '--disable-web-security',
93
- '--disable-features=BlockInsecurePrivateNetworkRequests'
94
- ]
95
- });
96
- const context = yield browser.newContext({ ignoreHTTPSErrors: true });
97
- const page = yield context.newPage();
98
- try {
99
- console.log(`🌐 런처 접속 중: ${launcherUrl}`);
100
- yield page.goto(launcherUrl);
101
- console.log(`⌨️ TV IP 입력 중: ${tvIp}`);
102
- yield page.waitForSelector('input[placeholder*="IP"]');
103
- yield page.fill('input[placeholder*="IP"]', tvIp);
104
- // 스크린샷 10.34.07 반영: Start Pairing 버튼 클릭
105
- console.log('🔘 Start Pairing 클릭');
106
- yield page.locator('input[value="Start Pairing"]').click();
107
- // SSL 인증 자동화 (스크린샷 10.25.20, 10.25.29 대응)
108
- const sslLink = page.locator('a', { hasText: 'Start SSL process' });
109
- if (yield sslLink.isVisible({ timeout: 5000 })) {
110
- console.log('⏳ SSL 인증 페이지 자동 돌파 시도...');
111
- const [sslPage] = yield Promise.all([
112
- context.waitForEvent('page'),
113
- sslLink.click()
114
- ]);
115
- if (sslPage) {
116
- yield sslPage.waitForLoadState('domcontentloaded');
117
- try {
118
- yield sslPage.locator('#details-button').click({ timeout: 5000 });
119
- yield sslPage.locator('#proceed-link').click({ timeout: 5000 });
120
- console.log('✅ SSL 인증 수락 완료.');
121
- yield sslPage.waitForTimeout(1000);
122
- yield sslPage.close();
123
- }
124
- catch (e) {
125
- console.log('ℹ️ SSL 경고창이 이미 승인되었거나 뜨지 않았습니다.');
126
- }
127
- }
128
- }
129
- // Continue 클릭 (이미지 10.25.20의 3단계)
130
- const continueBtn = page.locator('a', { hasText: 'Continue' });
131
- if (yield continueBtn.isVisible()) {
132
- yield continueBtn.click();
133
- }
134
- console.log('----------------------------------------------------');
135
- console.log('📺 TV 화면의 PIN 4자리를 입력하세요.');
136
- const pin = yield rl.question('PIN: ');
137
- console.log('----------------------------------------------------');
138
- // PIN 입력창 대기 및 입력
139
- yield page.waitForSelector('input[placeholder*="PIN"]');
140
- yield page.fill('input[placeholder*="PIN"]', pin);
141
- // [스크린샷 10.39.39 반영] Pair 버튼 클릭 (value="Pair")
142
- console.log('🔘 Pair 버튼 클릭');
143
- yield page.locator('input[value="Pair"]').click();
144
- console.log(`🔗 App URL 주입 중: ${appUrl}`);
145
- // 페어링 완료 후 App URL 입력 필드가 나타날 때까지 대기
146
- const urlInput = page.locator('#app-url');
147
- yield urlInput.waitFor({ state: 'visible', timeout: 30000 });
148
- // 기존 텍스트가 있을 수 있으므로 fill 대신 clear 후 fill 권장
149
- yield urlInput.click(); // 스크린샷의 onclick="this.select();" 대응
150
- yield urlInput.fill(appUrl);
151
- console.log(`✅ URL 주입 완료: ${appUrl}`);
152
- // [스크린샷 반영] Launch 버튼 클릭 (id="launch-btn")
153
- console.log('🚀 Launch 버튼 클릭!');
154
- yield page.locator('#launch-btn').click();
155
- console.log('✅ 모든 명령이 성공적으로 전달되었습니다!');
156
- }
157
- catch (err) {
158
- console.error('❌ 실행 중 에러 발생:', err);
159
- }
160
- });
161
- }
package/dist/webos.js DELETED
@@ -1,87 +0,0 @@
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 util_1 = require("./util");
18
- const fs_extra_1 = __importDefault(require("fs-extra"));
19
- const path_1 = __importDefault(require("path"));
20
- function runWebOS() {
21
- return __awaiter(this, void 0, void 0, function* () {
22
- var _a, _b;
23
- const args = process.argv.slice(2);
24
- const config = (0, util_1.loadConfig)();
25
- const getArg = (name) => { var _a; return (_a = args.find(arg => arg.startsWith(`--${name}=`))) === null || _a === void 0 ? void 0 : _a.split('=')[1]; };
26
- // 1. 필수 값 추출 (기본값 제거)
27
- const deviceName = getArg('device') || ((_a = config.webos) === null || _a === void 0 ? void 0 : _a.deviceName);
28
- const appId = getArg('appId') || ((_b = config.webos) === null || _b === void 0 ? void 0 : _b.appId);
29
- const targetArg = getArg('target');
30
- const appUrl = getArg('url') || `http://${(0, util_1.getLocalIp)()}:5173`;
31
- // 2. 필수 값 체크 (appId와 deviceName이 없으면 실행 중단)
32
- if (!deviceName || !appId) {
33
- console.error(`\n❌ 필수 설정이 누락되었습니다!`);
34
- console.log(`\n[해결 방법]`);
35
- console.log(`1. ctv.config.json에 'webos.deviceName'과 'webos.appId'를 설정하세요.`);
36
- console.log(`2. 또는 실행 시 인자를 추가하세요: ctv-run webos --device=lg-tv --appId=com.my.app`);
37
- return;
38
- }
39
- const tempAppDir = path_1.default.resolve(process.cwd(), '.ctv-temp-webos');
40
- try {
41
- if (fs_extra_1.default.existsSync(tempAppDir))
42
- fs_extra_1.default.removeSync(tempAppDir);
43
- console.log(`🛠️ Generating webOS app structure...`);
44
- // appId를 동적으로 받아 생성
45
- (0, child_process_1.execSync)(`ares-generate -t basic -p "id=${appId},title=CTV-App" ${tempAppDir}`, { stdio: 'inherit' });
46
- if (targetArg) {
47
- // [이사 모드] 빌드된 파일 복사
48
- const distPath = path_1.default.resolve(process.cwd(), targetArg);
49
- if (!fs_extra_1.default.existsSync(distPath))
50
- throw new Error(`❌ 폴더를 찾을 수 없습니다: ${distPath}`);
51
- console.log(`📦 [Static Mode] Copying from ${distPath}...`);
52
- fs_extra_1.default.copySync(distPath, tempAppDir, { overwrite: true });
53
- }
54
- else {
55
- // [창문 모드] 리다이렉트 HTML 생성
56
- console.log(`🌐 [Live Mode] Redirecting TV to ${appUrl}...`);
57
- const redirectHtml = `
58
- <!DOCTYPE html>
59
- <html>
60
- <head>
61
- <meta charset="utf-8">
62
- <script type="text/javascript">
63
- window.onload = function() { window.location.href = "${appUrl}"; };
64
- </script>
65
- </head>
66
- <body style="background:#000;color:#fff;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;">
67
- <h2>🚀 Connecting to Dev Server...</h2>
68
- </body>
69
- </html>
70
- `;
71
- fs_extra_1.default.writeFileSync(path_1.default.join(tempAppDir, 'index.html'), redirectHtml);
72
- }
73
- console.log(`🔨 Packaging & Installing [${appId}]...`);
74
- (0, child_process_1.execSync)(`ares-package ${tempAppDir}`, { stdio: 'inherit' });
75
- const ipkFile = fs_extra_1.default.readdirSync(process.cwd()).find(f => f.startsWith(appId) && f.endsWith('.ipk'));
76
- if (ipkFile) {
77
- (0, child_process_1.execSync)(`ares-install -d ${deviceName} ${ipkFile}`, { stdio: 'inherit' });
78
- (0, child_process_1.execSync)(`ares-launch -d ${deviceName} ${appId}`, { stdio: 'inherit' });
79
- fs_extra_1.default.removeSync(path_1.default.resolve(process.cwd(), ipkFile));
80
- console.log(`\n✅ webOS Launch Success!`);
81
- }
82
- }
83
- catch (err) {
84
- console.error(`\n❌ Error: ${err.message}`);
85
- }
86
- });
87
- }