@putdotio/taizn 1.2.0 → 1.4.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/dist/taizn.mjs CHANGED
@@ -1,72 +1,207 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "@effect/cli";
3
- import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
- import { Effect } from "effect";
5
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
- import * as ParseResult from "effect/ParseResult";
7
- import * as Schema from "effect/Schema";
8
- import { execFileSync } from "node:child_process";
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { Argument, CliError, Command, Flag } from "effect/unstable/cli";
4
+ import { Console, Context, Effect, FileSystem, Layer, Option, Schema, Stream } from "effect";
5
+ import { fileURLToPath } from "node:url";
9
6
  import { homedir } from "node:os";
10
- import { isAbsolute, join } from "node:path";
7
+ import { existsSync } from "node:fs";
8
+ import { dirname, isAbsolute, join } from "node:path";
9
+ import WebSocket from "ws";
10
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
11
+ //#region src/errors.ts
12
+ var ConfigNotFound = class extends Schema.TaggedErrorClass()("ConfigNotFound", { path: Schema.String }) {
13
+ get message() {
14
+ return `Config file not found: ${this.path}`;
15
+ }
16
+ };
17
+ var InvalidConfig = class extends Schema.TaggedErrorClass()("InvalidConfig", { details: Schema.String }) {
18
+ get message() {
19
+ return `Invalid taizn.json:\n${this.details}`;
20
+ }
21
+ };
22
+ var InvalidEnvironment = class extends Schema.TaggedErrorClass()("InvalidEnvironment", { details: Schema.String }) {
23
+ get message() {
24
+ return `Invalid TAIZN environment:\n${this.details}`;
25
+ }
26
+ };
27
+ var InvalidJson = class extends Schema.TaggedErrorClass()("InvalidJson", {
28
+ file: Schema.String,
29
+ details: Schema.String
30
+ }) {
31
+ get message() {
32
+ return `Invalid ${this.file}: ${this.details}`;
33
+ }
34
+ };
35
+ var MissingFile = class extends Schema.TaggedErrorClass()("MissingFile", {
36
+ label: Schema.String,
37
+ path: Schema.String
38
+ }) {
39
+ get message() {
40
+ return `${this.label} not found: ${this.path}`;
41
+ }
42
+ };
43
+ var FileSystemFailure = class extends Schema.TaggedErrorClass()("FileSystemFailure", {
44
+ operation: Schema.String,
45
+ path: Schema.String,
46
+ cause: Schema.Defect
47
+ }) {
48
+ get message() {
49
+ return `File system ${this.operation} failed for ${this.path}`;
50
+ }
51
+ };
52
+ var CommandFailed = class extends Schema.TaggedErrorClass()("CommandFailed", {
53
+ command: Schema.String,
54
+ args: Schema.Array(Schema.String)
55
+ }) {
56
+ get message() {
57
+ return `Command failed: ${this.command} ${this.args.join(" ")}`;
58
+ }
59
+ };
60
+ var PackageNotProduced = class extends Schema.TaggedErrorClass()("PackageNotProduced", { outputDir: Schema.String }) {
61
+ get message() {
62
+ return `No .wgt package was produced in ${this.outputDir}`;
63
+ }
64
+ };
65
+ var MissingPassword = class extends Schema.TaggedErrorClass()("MissingPassword", {
66
+ variable: Schema.String,
67
+ action: Schema.String
68
+ }) {
69
+ get message() {
70
+ return `${this.variable} is required to ${this.action}.`;
71
+ }
72
+ };
73
+ var SecretReadInterrupted = class extends Schema.TaggedErrorClass()("SecretReadInterrupted", {}) {
74
+ get message() {
75
+ return "Secret prompt interrupted.";
76
+ }
77
+ };
78
+ var MultipleTargetsConnected = class extends Schema.TaggedErrorClass()("MultipleTargetsConnected", { targets: Schema.Array(Schema.String) }) {
79
+ get message() {
80
+ return `Multiple Tizen targets are connected: ${this.targets.join(", ")}. Set TAIZN_TARGET explicitly.`;
81
+ }
82
+ };
83
+ var MissingTizenTarget = class extends Schema.TaggedErrorClass()("MissingTizenTarget", {}) {
84
+ get message() {
85
+ return "No Tizen target is connected. Set TAIZN_TARGET or connect exactly one device.";
86
+ }
87
+ };
88
+ var MissingTvRemoteHost = class extends Schema.TaggedErrorClass()("MissingTvRemoteHost", {}) {
89
+ get message() {
90
+ return "Samsung TV host is required. Set TAIZN_TV_HOST or TAIZN_TARGET.";
91
+ }
92
+ };
93
+ var MissingTvRemoteToken = class extends Schema.TaggedErrorClass()("MissingTvRemoteToken", {}) {
94
+ get message() {
95
+ return "Samsung TV remote token is required. Run `taizn tv pair` or set TAIZN_TV_TOKEN.";
96
+ }
97
+ };
98
+ var TvRemoteConnectionFailed = class extends Schema.TaggedErrorClass()("TvRemoteConnectionFailed", {
99
+ cause: Schema.Defect,
100
+ target: Schema.String
101
+ }) {
102
+ get message() {
103
+ return `Samsung TV remote connection failed: ${this.target}`;
104
+ }
105
+ };
106
+ var TvRemoteProtocolError = class extends Schema.TaggedErrorClass()("TvRemoteProtocolError", { details: Schema.String }) {
107
+ get message() {
108
+ return `Samsung TV remote protocol error: ${this.details}`;
109
+ }
110
+ };
111
+ var TvRemoteTimeout = class extends Schema.TaggedErrorClass()("TvRemoteTimeout", { target: Schema.String }) {
112
+ get message() {
113
+ return `Timed out waiting for Samsung TV remote response: ${this.target}`;
114
+ }
115
+ };
116
+ var TvRemoteUnauthorized = class extends Schema.TaggedErrorClass()("TvRemoteUnauthorized", { target: Schema.String }) {
117
+ get message() {
118
+ return `Samsung TV denied remote control access: ${this.target}`;
119
+ }
120
+ };
121
+ const renderError = (error) => error.message;
122
+ //#endregion
11
123
  //#region src/runtime.ts
12
- const appDir = process.cwd();
13
- const configPath = join(appDir, "taizn.json");
14
- const taiznDir = join(appDir, ".taizn");
15
- const envPath = join(taiznDir, ".env");
16
- const stageDir = join(taiznDir, "build", "stage");
17
- const outputDir = join(taiznDir, "build", "output");
18
- const appPath = (path) => isAbsolute(path) ? path : join(appDir, path);
19
- const loadLocalEnv = () => {
20
- if (existsSync(envPath)) process.loadEnvFile(envPath);
124
+ var TaiznSystem = class TaiznSystem extends Context.Service()("taizn/TaiznSystem") {
125
+ static Live = Layer.succeed(TaiznSystem)({
126
+ cwd: Effect.sync(() => process.cwd()),
127
+ env: Effect.sync(() => process.env),
128
+ homeDir: Effect.sync(() => homedir()),
129
+ loadEnvFile: (path) => Effect.sync(() => {
130
+ if (existsSync(path)) process.loadEnvFile(path);
131
+ }),
132
+ readSecret: (prompt) => Effect.tryPromise({
133
+ try: () => readSecret(prompt),
134
+ catch: () => SecretReadInterrupted.make({})
135
+ })
136
+ });
21
137
  };
22
- const fail = (message) => {
23
- console.error(message);
24
- process.exit(1);
138
+ const makePaths = (appDir) => {
139
+ const taiznDir = join(appDir, ".taizn");
140
+ return {
141
+ appDir,
142
+ configPath: join(appDir, "taizn.json"),
143
+ envPath: join(taiznDir, ".env"),
144
+ outputDir: join(taiznDir, "build", "output"),
145
+ remoteStatePath: join(taiznDir, "remote.json"),
146
+ stageDir: join(taiznDir, "build", "stage"),
147
+ taiznDir
148
+ };
25
149
  };
26
- const requireFile = (path, label) => {
27
- if (!existsSync(path)) fail(`${label} not found: ${path}`);
150
+ const getPaths = Effect.fn("getPaths")(function* () {
151
+ return makePaths(yield* (yield* TaiznSystem).cwd);
152
+ });
153
+ const appPath = (appDir, path) => isAbsolute(path) ? path : join(appDir, path);
154
+ const loadLocalEnv = Effect.fn("loadLocalEnv")(function* () {
155
+ const paths = yield* getPaths();
156
+ yield* (yield* TaiznSystem).loadEnvFile(paths.envPath);
157
+ });
158
+ const requireFile = Effect.fn("requireFile")(function* (path, label) {
159
+ if (!(yield* (yield* FileSystem.FileSystem).exists(path).pipe(Effect.mapError((cause) => FileSystemFailure.make({
160
+ cause,
161
+ operation: "exists",
162
+ path
163
+ }))))) return yield* MissingFile.make({
164
+ label,
165
+ path
166
+ });
28
167
  return path;
29
- };
30
- const baseChildEnv = () => {
31
- const env = { ...process.env };
168
+ });
169
+ const baseChildEnv = Effect.fn("baseChildEnv")(function* () {
170
+ const env = { ...yield* (yield* TaiznSystem).env };
32
171
  delete env.DYLD_INSERT_LIBRARIES;
33
172
  return env;
34
- };
35
- const appBuildEnv = () => {
36
- const env = baseChildEnv();
173
+ });
174
+ const appBuildEnv = Effect.fn("appBuildEnv")(function* () {
175
+ const env = yield* baseChildEnv();
37
176
  for (const key of Object.keys(env)) if (key.startsWith("TAIZN_") || key.startsWith("TIZEN_")) delete env[key];
38
177
  delete env.SDB;
39
178
  return env;
40
- };
41
- const run = (command, args, options = {}) => {
42
- const env = options.env || baseChildEnv();
43
- try {
44
- execFileSync(command, args, {
45
- cwd: options.cwd || appDir,
46
- env: {
47
- ...env,
48
- PATH: `${join(homedir(), "tizen-studio/tools/ide/bin")}:${join(homedir(), "tizen-studio/tools")}:${env.PATH || ""}`
49
- },
50
- stdio: options.stdio || "inherit"
51
- });
52
- } catch {
53
- fail(`Command failed: ${command} ${redactCommandArgs(args).join(" ")}`);
54
- }
55
- };
56
- const tizenCli = (path) => requireFile(path || join(homedir(), "tizen-studio/tools/ide/bin/tizen"), "Tizen CLI");
57
- const sdb = (path) => requireFile(path || join(homedir(), "tizen-studio/tools/sdb"), "sdb");
58
- const readPassword = async (value, prompt) => {
179
+ });
180
+ const withTizenPath = Effect.fn("withTizenPath")(function* (env) {
181
+ const home = yield* (yield* TaiznSystem).homeDir;
182
+ return {
183
+ ...env,
184
+ PATH: `${join(home, "tizen-studio/tools/ide/bin")}:${join(home, "tizen-studio/tools")}:${env.PATH ?? ""}`
185
+ };
186
+ });
187
+ const defaultTizenCli = Effect.fn("defaultTizenCli")(function* () {
188
+ return join(yield* (yield* TaiznSystem).homeDir, "tizen-studio/tools/ide/bin/tizen");
189
+ });
190
+ const defaultSdb = Effect.fn("defaultSdb")(function* () {
191
+ return join(yield* (yield* TaiznSystem).homeDir, "tizen-studio/tools/sdb");
192
+ });
193
+ const readPassword = Effect.fn("readPassword")(function* (value, prompt) {
59
194
  if (value) return value;
60
- return readSecret(prompt);
61
- };
195
+ return yield* (yield* TaiznSystem).readSecret(prompt);
196
+ });
62
197
  const redactCommandArgs = (args) => {
63
198
  const sensitiveValueFlags = new Set(["-p", "-dp"]);
64
199
  return args.map((arg, index) => {
65
- if (index > 0 && sensitiveValueFlags.has(args[index - 1])) return "[redacted]";
200
+ if (index > 0 && sensitiveValueFlags.has(args[index - 1] ?? "")) return "[redacted]";
66
201
  return arg;
67
202
  });
68
203
  };
69
- const readSecret = (prompt) => new Promise((resolve) => {
204
+ const readSecret = (prompt) => new Promise((resolve, reject) => {
70
205
  if (!process.stdin.isTTY) {
71
206
  resolve("");
72
207
  return;
@@ -76,15 +211,20 @@ const readSecret = (prompt) => new Promise((resolve) => {
76
211
  process.stdin.setRawMode(true);
77
212
  process.stdin.resume();
78
213
  process.stdin.setEncoding("utf8");
214
+ const cleanup = () => {
215
+ process.stdin.setRawMode(false);
216
+ process.stdin.pause();
217
+ process.stdin.off("data", onData);
218
+ };
79
219
  const onData = (char) => {
80
220
  if (char === "") {
221
+ cleanup();
81
222
  process.stdout.write("\n");
82
- process.exit(130);
223
+ reject(/* @__PURE__ */ new Error("interrupted"));
224
+ return;
83
225
  }
84
226
  if (char === "\r" || char === "\n") {
85
- process.stdin.setRawMode(false);
86
- process.stdin.pause();
87
- process.stdin.off("data", onData);
227
+ cleanup();
88
228
  process.stdout.write("\n");
89
229
  resolve(value);
90
230
  return;
@@ -99,7 +239,7 @@ const readSecret = (prompt) => new Promise((resolve) => {
99
239
  });
100
240
  //#endregion
101
241
  //#region src/config.ts
102
- const TizenVariantSchema = Schema.Struct({
242
+ var TizenVariant = class extends Schema.Class("TizenVariant")({
103
243
  applicationId: Schema.NonEmptyString,
104
244
  bundleName: Schema.NonEmptyString,
105
245
  excludeFiles: Schema.optional(Schema.Array(Schema.NonEmptyString)),
@@ -109,82 +249,440 @@ const TizenVariantSchema = Schema.Struct({
109
249
  name: Schema.NonEmptyString,
110
250
  packageId: Schema.NonEmptyString,
111
251
  rewriteAssetUrls: Schema.optional(Schema.Boolean)
252
+ }) {};
253
+ var BuildConfig = class extends Schema.Class("BuildConfig")({
254
+ command: Schema.NonEmptyArray(Schema.NonEmptyString),
255
+ output: Schema.NonEmptyString,
256
+ requiredFiles: Schema.optional(Schema.Array(Schema.NonEmptyString))
257
+ }) {};
258
+ var SigningConfig = class extends Schema.Class("SigningConfig")({
259
+ certificateDir: Schema.NonEmptyString,
260
+ profile: Schema.NonEmptyString
261
+ }) {};
262
+ var WidgetVariants = class extends Schema.Class("WidgetVariants")({
263
+ development: TizenVariant,
264
+ production: TizenVariant
265
+ }) {};
266
+ var WidgetConfig = class extends Schema.Class("WidgetConfig")({
267
+ configXml: Schema.NonEmptyString,
268
+ excludeFiles: Schema.optional(Schema.Array(Schema.NonEmptyString)),
269
+ indexHtml: Schema.NonEmptyString,
270
+ injectWebapis: Schema.optional(Schema.Boolean),
271
+ rewriteAssetUrls: Schema.optional(Schema.Boolean),
272
+ variants: WidgetVariants
273
+ }) {};
274
+ var TizenConfig = class extends Schema.Class("TizenConfig")({
275
+ build: BuildConfig,
276
+ signing: SigningConfig,
277
+ widget: WidgetConfig
278
+ }) {};
279
+ const loadConfig = Effect.fn("loadConfig")(function* () {
280
+ const fs = yield* FileSystem.FileSystem;
281
+ const paths = yield* getPaths();
282
+ if (!(yield* fs.exists(paths.configPath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
283
+ cause,
284
+ operation: "exists",
285
+ path: paths.configPath
286
+ }))))) return yield* ConfigNotFound.make({ path: paths.configPath });
287
+ return yield* decodeConfig(yield* fs.readFileString(paths.configPath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
288
+ cause,
289
+ operation: "read",
290
+ path: paths.configPath
291
+ }))));
112
292
  });
113
- const TizenConfigSchema = Schema.Struct({
114
- build: Schema.Struct({
115
- command: Schema.NonEmptyArray(Schema.NonEmptyString),
116
- output: Schema.NonEmptyString,
117
- requiredFiles: Schema.optional(Schema.Array(Schema.NonEmptyString))
118
- }),
119
- signing: Schema.Struct({
120
- certificateDir: Schema.NonEmptyString,
121
- profile: Schema.NonEmptyString
122
- }),
123
- widget: Schema.Struct({
124
- configXml: Schema.NonEmptyString,
125
- excludeFiles: Schema.optional(Schema.Array(Schema.NonEmptyString)),
126
- indexHtml: Schema.NonEmptyString,
127
- injectWebapis: Schema.optional(Schema.Boolean),
128
- rewriteAssetUrls: Schema.optional(Schema.Boolean),
129
- variants: Schema.Struct({
130
- development: TizenVariantSchema,
131
- production: TizenVariantSchema
293
+ const decodeConfig = Effect.fn("decodeConfig")(function* (source) {
294
+ const json = yield* Effect.try({
295
+ try: () => JSON.parse(source),
296
+ catch: (cause) => InvalidJson.make({
297
+ details: causeToMessage$1(cause),
298
+ file: "taizn.json"
132
299
  })
133
- })
300
+ });
301
+ return yield* Schema.decodeUnknownEffect(TizenConfig)(json, { errors: "all" }).pipe(Effect.mapError((error) => InvalidConfig.make({ details: error.message })));
134
302
  });
135
- const loadConfig = () => {
136
- if (!existsSync(configPath)) fail(`Config file not found: ${configPath}`);
137
- return decodeConfig(readFileSync(configPath, "utf8"));
138
- };
139
- const decodeConfig = (source) => {
140
- try {
141
- return Schema.decodeUnknownSync(Schema.parseJson(TizenConfigSchema), { errors: "all" })(source);
142
- } catch (error) {
143
- if (ParseResult.isParseError(error)) return fail(`Invalid taizn.json:\n${formatParseIssues$1(error)}`);
144
- return fail(`Invalid taizn.json: ${error instanceof Error ? error.message : String(error)}`);
145
- }
146
- };
147
- const formatParseIssues$1 = (error) => ParseResult.ArrayFormatter.formatErrorSync(error).map((issue) => {
148
- return `- ${issue.path.length > 0 ? issue.path.join(".") : "taizn.json"}: ${issue.message}`;
149
- }).join("\n");
303
+ const causeToMessage$1 = (cause) => cause instanceof Error ? cause.message : String(cause);
150
304
  //#endregion
151
305
  //#region src/env.ts
152
- const TaiznEnvSchema = Schema.Struct({
306
+ const RawTaiznEnv = Schema.Struct({
153
307
  certPassword: Schema.optional(Schema.String),
154
308
  distPassword: Schema.optional(Schema.String),
155
309
  sdb: Schema.optional(Schema.String),
156
310
  target: Schema.optional(Schema.String),
157
311
  tizenCli: Schema.optional(Schema.String),
158
- variant: Schema.optional(Schema.Literal("development", "production"))
312
+ tvHost: Schema.optional(Schema.String),
313
+ tvInfoPort: Schema.optional(Schema.String),
314
+ tvName: Schema.optional(Schema.String),
315
+ tvPort: Schema.optional(Schema.String),
316
+ tvProtocol: Schema.optional(Schema.Literals(["ws", "wss"])),
317
+ tvTimeoutMs: Schema.optional(Schema.String),
318
+ tvToken: Schema.optional(Schema.String),
319
+ variant: Schema.optional(Schema.Literals(["development", "production"]))
159
320
  });
160
- const loadEnv = () => {
161
- try {
162
- const env = Schema.decodeUnknownSync(TaiznEnvSchema)({
163
- certPassword: process.env.TAIZN_CERT_PASSWORD,
164
- distPassword: process.env.TAIZN_DIST_PASSWORD,
165
- sdb: process.env.TAIZN_SDB,
166
- target: process.env.TAIZN_TARGET,
167
- tizenCli: process.env.TAIZN_TIZEN_CLI,
168
- variant: process.env.TAIZN_VARIANT
169
- });
170
- return {
171
- ...env,
172
- variant: env.variant ?? "development"
321
+ var TaiznEnv = class extends Schema.Class("TaiznEnv")({
322
+ certPassword: Schema.optional(Schema.String),
323
+ distPassword: Schema.optional(Schema.String),
324
+ sdb: Schema.optional(Schema.String),
325
+ target: Schema.optional(Schema.String),
326
+ tizenCli: Schema.optional(Schema.String),
327
+ tvHost: Schema.optional(Schema.String),
328
+ tvInfoPort: Schema.optional(Schema.Number),
329
+ tvName: Schema.optional(Schema.String),
330
+ tvPort: Schema.optional(Schema.Number),
331
+ tvProtocol: Schema.optional(Schema.Literals(["ws", "wss"])),
332
+ tvTimeoutMs: Schema.optional(Schema.Number),
333
+ tvToken: Schema.optional(Schema.String),
334
+ variant: Schema.Literals(["development", "production"])
335
+ }) {};
336
+ const loadEnv = Effect.fn("loadEnv")(function* () {
337
+ const env = yield* (yield* TaiznSystem).env;
338
+ const raw = yield* Schema.decodeUnknownEffect(RawTaiznEnv)({
339
+ certPassword: env.TAIZN_CERT_PASSWORD,
340
+ distPassword: env.TAIZN_DIST_PASSWORD,
341
+ sdb: env.TAIZN_SDB,
342
+ target: env.TAIZN_TARGET,
343
+ tizenCli: env.TAIZN_TIZEN_CLI,
344
+ tvHost: env.TAIZN_TV_HOST,
345
+ tvInfoPort: env.TAIZN_TV_INFO_PORT,
346
+ tvName: env.TAIZN_TV_NAME,
347
+ tvPort: env.TAIZN_TV_PORT,
348
+ tvProtocol: env.TAIZN_TV_PROTOCOL,
349
+ tvTimeoutMs: env.TAIZN_TV_TIMEOUT_MS,
350
+ tvToken: env.TAIZN_TV_TOKEN,
351
+ variant: env.TAIZN_VARIANT
352
+ }, { errors: "all" }).pipe(Effect.mapError((error) => InvalidEnvironment.make({ details: error.message })));
353
+ const tvInfoPort = raw.tvInfoPort ? yield* parsePort(raw.tvInfoPort, "TAIZN_TV_INFO_PORT") : void 0;
354
+ const tvPort = raw.tvPort ? yield* parseTvPort(raw.tvPort) : void 0;
355
+ const tvTimeoutMs = raw.tvTimeoutMs ? yield* parsePositiveInteger(raw.tvTimeoutMs, "TAIZN_TV_TIMEOUT_MS") : void 0;
356
+ return TaiznEnv.make({
357
+ ...raw,
358
+ tvInfoPort,
359
+ tvPort,
360
+ tvTimeoutMs,
361
+ variant: raw.variant ?? "development"
362
+ });
363
+ });
364
+ const parseTvPort = Effect.fn("parseTvPort")(function* (value) {
365
+ return yield* parsePort(value, "TAIZN_TV_PORT");
366
+ });
367
+ const parsePort = Effect.fn("parsePort")(function* (value, variable) {
368
+ const port = Number(value);
369
+ if (!/^\d+$/.test(value) || !Number.isInteger(port) || port < 1 || port > 65535) return yield* InvalidEnvironment.make({ details: `${variable} must be an integer between 1 and 65535. Received: ${value}` });
370
+ return port;
371
+ });
372
+ const parsePositiveInteger = Effect.fn("parsePositiveInteger")(function* (value, variable) {
373
+ const parsed = Number(value);
374
+ if (!/^\d+$/.test(value) || !Number.isInteger(parsed) || parsed < 1) return yield* InvalidEnvironment.make({ details: `${variable} must be a positive integer. Received: ${value}` });
375
+ return parsed;
376
+ });
377
+ //#endregion
378
+ //#region src/context.ts
379
+ const loadContext = Effect.fn("loadContext")(function* () {
380
+ return {
381
+ config: yield* loadConfig(),
382
+ env: yield* loadEnv()
383
+ };
384
+ });
385
+ //#endregion
386
+ //#region src/remote.ts
387
+ const DEFAULT_REMOTE_NAME = "taizn";
388
+ const DEFAULT_REMOTE_PORT = 8002;
389
+ const DEFAULT_REMOTE_PROTOCOL = "wss";
390
+ const DEFAULT_TIMEOUT_MS = 3e4;
391
+ const TV_INFO_PORT = 8001;
392
+ var RemoteClientAttributes = class extends Schema.Class("RemoteClientAttributes")({
393
+ name: Schema.optional(Schema.String),
394
+ token: Schema.optional(Schema.String)
395
+ }) {};
396
+ var RemoteClient = class extends Schema.Class("RemoteClient")({
397
+ attributes: Schema.optional(RemoteClientAttributes),
398
+ deviceName: Schema.optional(Schema.String),
399
+ id: Schema.String,
400
+ isHost: Schema.Boolean
401
+ }) {};
402
+ var RemoteEventData = class extends Schema.Class("RemoteEventData")({
403
+ clients: Schema.optional(Schema.Array(RemoteClient)),
404
+ id: Schema.optional(Schema.String),
405
+ token: Schema.optional(Schema.String)
406
+ }) {};
407
+ var RemoteEvent = class extends Schema.Class("RemoteEvent")({
408
+ data: Schema.optional(RemoteEventData),
409
+ event: Schema.String
410
+ }) {};
411
+ var TvInfoDevice = class extends Schema.Class("TvInfoDevice")({
412
+ TokenAuthSupport: Schema.optional(Schema.String),
413
+ developerIP: Schema.optional(Schema.String),
414
+ developerMode: Schema.optional(Schema.String),
415
+ ip: Schema.optional(Schema.String),
416
+ modelName: Schema.optional(Schema.String),
417
+ name: Schema.optional(Schema.String)
418
+ }) {};
419
+ var TvInfo = class extends Schema.Class("TvInfo")({
420
+ device: TvInfoDevice,
421
+ isSupport: Schema.optional(Schema.String),
422
+ name: Schema.String,
423
+ remote: Schema.optional(Schema.String),
424
+ type: Schema.optional(Schema.String),
425
+ uri: Schema.optional(Schema.String)
426
+ }) {};
427
+ var TvRemoteState = class extends Schema.Class("TvRemoteState")({
428
+ host: Schema.String,
429
+ name: Schema.String,
430
+ port: Schema.Number,
431
+ protocol: Schema.Literals(["ws", "wss"]),
432
+ token: Schema.String
433
+ }) {};
434
+ const pairSamsungTvRemote = Effect.fn("pairSamsungTvRemote")(function* (env) {
435
+ const options = yield* resolveRemoteOptions(env, { ignoreToken: true });
436
+ yield* Console.log(`Pairing Samsung TV remote: ${remoteTarget(options)}`);
437
+ yield* Console.log("Accept the remote control prompt on the TV if it appears.");
438
+ const token = yield* connectRemote(options);
439
+ yield* saveRemoteState({
440
+ ...options,
441
+ token
442
+ });
443
+ yield* Console.log(`Saved Samsung TV remote token to .taizn/remote.json`);
444
+ yield* Console.log(`TAIZN_TV_TOKEN=${token}`);
445
+ });
446
+ Effect.fn("sendSamsungTvKey")(function* (env, key) {
447
+ yield* sendSamsungTvKeys(env, [key]);
448
+ });
449
+ const sendSamsungTvKeys = Effect.fn("sendSamsungTvKeys")(function* (env, keys, pressOptions) {
450
+ const remoteOptions = yield* resolveRemoteOptions(env, { requireToken: true });
451
+ if (!remoteOptions.token) return yield* MissingTvRemoteToken.make({});
452
+ yield* connectRemote(remoteOptions, {
453
+ delayMs: Math.max(0, pressOptions?.delayMs ?? 250),
454
+ keys
455
+ });
456
+ yield* Console.log(keys.length === 1 ? `Sent Samsung TV remote key: ${keys[0]}` : `Sent Samsung TV remote keys: ${keys.join(", ")}`);
457
+ });
458
+ const showSamsungTvInfo = Effect.fn("showSamsungTvInfo")(function* (env) {
459
+ const options = yield* resolveRemoteOptions(env);
460
+ const info = yield* fetchSamsungTvInfo(options.host, {
461
+ port: env.tvInfoPort,
462
+ timeoutMs: options.timeoutMs
463
+ });
464
+ const support = info.isSupport ? parseSupport(info.isSupport) : void 0;
465
+ yield* Console.log(`Samsung TV: ${decodeHtml(info.name)}`);
466
+ yield* Console.log(`model: ${info.device.modelName ?? "unknown"}`);
467
+ yield* Console.log(`ip: ${info.device.ip ?? options.host}`);
468
+ yield* Console.log(`remote: ${info.remote ?? "unknown"}`);
469
+ yield* Console.log(`remote_available: ${support?.remote_available ?? "unknown"}`);
470
+ yield* Console.log(`token_auth: ${info.device.TokenAuthSupport ?? "unknown"}`);
471
+ yield* Console.log(`developer_ip: ${info.device.developerIP ?? "unknown"}`);
472
+ yield* Console.log(`developer_mode: ${info.device.developerMode ?? "unknown"}`);
473
+ });
474
+ const resolveRemoteOptions = Effect.fn("resolveRemoteOptions")(function* (env, options) {
475
+ const targetHost = hostFromTarget(env.target);
476
+ const state = !env.tvHost && !targetHost || Boolean(options?.requireToken && !options.ignoreToken && !env.tvToken) ? yield* readRemoteState() : void 0;
477
+ const host = env.tvHost ?? state?.host ?? targetHost;
478
+ if (!host) return yield* MissingTvRemoteHost.make({});
479
+ const token = options?.ignoreToken ? void 0 : env.tvToken ?? state?.token;
480
+ if (options?.requireToken && !token) return yield* MissingTvRemoteToken.make({});
481
+ return {
482
+ host,
483
+ name: env.tvName ?? state?.name ?? DEFAULT_REMOTE_NAME,
484
+ port: env.tvPort ?? state?.port ?? DEFAULT_REMOTE_PORT,
485
+ protocol: env.tvProtocol ?? state?.protocol ?? DEFAULT_REMOTE_PROTOCOL,
486
+ timeoutMs: env.tvTimeoutMs ?? DEFAULT_TIMEOUT_MS,
487
+ token
488
+ };
489
+ });
490
+ const readRemoteState = Effect.fn("readRemoteState")(function* () {
491
+ const fs = yield* FileSystem.FileSystem;
492
+ const paths = yield* getPaths();
493
+ if (!(yield* fs.exists(paths.remoteStatePath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
494
+ cause,
495
+ operation: "exists",
496
+ path: paths.remoteStatePath
497
+ }))))) return;
498
+ const source = yield* fs.readFileString(paths.remoteStatePath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
499
+ cause,
500
+ operation: "read",
501
+ path: paths.remoteStatePath
502
+ })));
503
+ const json = yield* Effect.try({
504
+ try: () => JSON.parse(source),
505
+ catch: (cause) => InvalidJson.make({
506
+ details: causeToMessage(cause),
507
+ file: paths.remoteStatePath
508
+ })
509
+ });
510
+ return yield* Schema.decodeUnknownEffect(TvRemoteState)(json, { errors: "all" }).pipe(Effect.mapError((error) => InvalidJson.make({
511
+ details: error.message,
512
+ file: paths.remoteStatePath
513
+ })));
514
+ });
515
+ const saveRemoteState = Effect.fn("saveRemoteState")(function* (options) {
516
+ const fs = yield* FileSystem.FileSystem;
517
+ const paths = yield* getPaths();
518
+ const state = TvRemoteState.make({
519
+ host: options.host,
520
+ name: options.name,
521
+ port: options.port,
522
+ protocol: options.protocol,
523
+ token: options.token
524
+ });
525
+ yield* fs.makeDirectory(dirname(paths.remoteStatePath), { recursive: true }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
526
+ cause,
527
+ operation: "mkdir",
528
+ path: dirname(paths.remoteStatePath)
529
+ })));
530
+ yield* fs.writeFileString(paths.remoteStatePath, `${JSON.stringify(state, null, 2)}\n`).pipe(Effect.mapError((cause) => FileSystemFailure.make({
531
+ cause,
532
+ operation: "write",
533
+ path: paths.remoteStatePath
534
+ })));
535
+ });
536
+ const connectRemote = Effect.fn("connectRemote")(function* (options, sequence) {
537
+ return yield* Effect.tryPromise({
538
+ try: () => connectRemotePromise(options, sequence),
539
+ catch: (cause) => normalizeRemoteError(cause, options)
540
+ });
541
+ });
542
+ const connectRemotePromise = (options, sequence) => new Promise((resolve, reject) => {
543
+ const ws = new WebSocket(remoteUrl(options), {
544
+ handshakeTimeout: options.timeoutMs,
545
+ rejectUnauthorized: false
546
+ });
547
+ const timer = setTimeout(() => {
548
+ reject(TvRemoteTimeout.make({ target: remoteTarget(options) }));
549
+ ws.terminate();
550
+ }, options.timeoutMs);
551
+ let settled = false;
552
+ const succeed = (token) => {
553
+ if (settled) return;
554
+ settled = true;
555
+ clearTimeout(timer);
556
+ resolve(token);
557
+ ws.close();
558
+ };
559
+ const fail = (error) => {
560
+ if (settled) return;
561
+ settled = true;
562
+ clearTimeout(timer);
563
+ reject(error);
564
+ ws.close();
565
+ };
566
+ const sendSequence = (token) => {
567
+ const keys = sequence?.keys ?? [];
568
+ if (keys.length === 0) {
569
+ succeed(token);
570
+ return;
571
+ }
572
+ let index = 0;
573
+ const sendNext = () => {
574
+ const key = keys[index];
575
+ if (!key) {
576
+ succeed(token);
577
+ return;
578
+ }
579
+ ws.send(JSON.stringify(remoteKeyPayload(key)));
580
+ index += 1;
581
+ if (index >= keys.length) {
582
+ setTimeout(() => succeed(token), 500);
583
+ return;
584
+ }
585
+ setTimeout(sendNext, sequence?.delayMs ?? 250);
173
586
  };
174
- } catch (error) {
175
- if (ParseResult.isParseError(error)) return fail(`Invalid TAIZN environment:\n${formatParseIssues(error)}`);
176
- return fail(`Invalid TAIZN environment: ${error instanceof Error ? error.message : String(error)}`);
587
+ sendNext();
588
+ };
589
+ ws.on("message", (data) => {
590
+ let event;
591
+ try {
592
+ event = parseRemoteEvent(data.toString(), options);
593
+ } catch (cause) {
594
+ fail(normalizeRemoteError(cause, options));
595
+ return;
596
+ }
597
+ if (event.event === "ms.channel.unauthorized") {
598
+ fail(TvRemoteUnauthorized.make({ target: remoteTarget(options) }));
599
+ return;
600
+ }
601
+ if (event.event !== "ms.channel.connect") return;
602
+ const token = remoteEventToken(event) ?? options.token;
603
+ if (!token) {
604
+ fail(TvRemoteProtocolError.make({ details: "connect event did not include a token" }));
605
+ return;
606
+ }
607
+ sendSequence(token);
608
+ });
609
+ ws.on("error", (cause) => {
610
+ fail(TvRemoteConnectionFailed.make({
611
+ cause,
612
+ target: remoteTarget(options)
613
+ }));
614
+ });
615
+ ws.on("close", () => {
616
+ if (!settled) fail(TvRemoteConnectionFailed.make({
617
+ cause: /* @__PURE__ */ new Error("remote websocket closed before connecting"),
618
+ target: remoteTarget(options)
619
+ }));
620
+ });
621
+ });
622
+ const fetchSamsungTvInfo = Effect.fn("fetchSamsungTvInfo")(function* (host, options) {
623
+ const url = `http://${host}:${options?.port ?? TV_INFO_PORT}/api/v2/`;
624
+ const json = yield* Effect.tryPromise({
625
+ try: async () => {
626
+ const response = await fetch(url, { signal: AbortSignal.timeout(options?.timeoutMs ?? DEFAULT_TIMEOUT_MS) });
627
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
628
+ return await response.json();
629
+ },
630
+ catch: (cause) => isAbortError(cause) ? TvRemoteTimeout.make({ target: url }) : TvRemoteConnectionFailed.make({
631
+ cause,
632
+ target: url
633
+ })
634
+ });
635
+ return yield* Schema.decodeUnknownEffect(TvInfo)(json, { errors: "all" }).pipe(Effect.mapError((error) => TvRemoteProtocolError.make({ details: error.message })));
636
+ });
637
+ const isAbortError = (cause) => cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError");
638
+ const parseRemoteEvent = (source, options) => {
639
+ try {
640
+ return Schema.decodeUnknownSync(RemoteEvent)(JSON.parse(source));
641
+ } catch (cause) {
642
+ throw TvRemoteProtocolError.make({ details: `${remoteTarget(options)} sent an unexpected message: ${causeToMessage(cause)}` });
177
643
  }
178
644
  };
179
- const formatParseIssues = (error) => ParseResult.ArrayFormatter.formatErrorSync(error).map((issue) => {
180
- return `- ${issue.path.length > 0 ? issue.path.join(".") : "TAIZN environment"}: ${issue.message}`;
181
- }).join("\n");
182
- //#endregion
183
- //#region src/context.ts
184
- const loadContext = () => ({
185
- config: loadConfig(),
186
- env: loadEnv()
645
+ const parseSupport = (source) => {
646
+ try {
647
+ return Schema.decodeUnknownSync(Schema.Struct({ remote_available: Schema.optional(Schema.String) }))(JSON.parse(source));
648
+ } catch {
649
+ return;
650
+ }
651
+ };
652
+ const remoteUrl = (options) => {
653
+ const params = new URLSearchParams({ name: Buffer.from(options.name).toString("base64") });
654
+ if (options.token) params.set("token", options.token);
655
+ return `${options.protocol}://${options.host}:${options.port}/api/v2/channels/samsung.remote.control?${params.toString()}`;
656
+ };
657
+ const remoteTarget = (options) => `${options.protocol}://${options.host}:${options.port}`;
658
+ const remoteKeyPayload = (key) => ({
659
+ method: "ms.remote.control",
660
+ params: {
661
+ Cmd: "Click",
662
+ DataOfCmd: key,
663
+ Option: "false",
664
+ TypeOfRemote: "SendRemoteKey"
665
+ }
187
666
  });
667
+ const remoteEventToken = (event) => {
668
+ const data = event.data;
669
+ if (!data) return;
670
+ const matchingClientToken = data.id ? data.clients?.find((client) => client.id === data.id)?.attributes?.token : void 0;
671
+ return data.token ?? matchingClientToken ?? data.clients?.find((client) => client.attributes?.token)?.attributes?.token;
672
+ };
673
+ const normalizeRemoteError = (cause, options) => {
674
+ if (cause instanceof MissingTvRemoteHost || cause instanceof MissingTvRemoteToken || cause instanceof TvRemoteConnectionFailed || cause instanceof TvRemoteProtocolError || cause instanceof TvRemoteTimeout || cause instanceof TvRemoteUnauthorized) return cause;
675
+ return TvRemoteConnectionFailed.make({
676
+ cause,
677
+ target: remoteTarget(options)
678
+ });
679
+ };
680
+ const hostFromTarget = (target) => {
681
+ if (!target) return;
682
+ return target.split(":").at(0);
683
+ };
684
+ const causeToMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
685
+ const decodeHtml = (value) => value.replaceAll("&quot;", "\"").replaceAll("&amp;", "&");
188
686
  //#endregion
189
687
  //#region src/xml.ts
190
688
  const escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -196,25 +694,28 @@ const setXmlAttribute = (tag, attribute, value) => {
196
694
  };
197
695
  //#endregion
198
696
  //#region src/tizen.ts
199
- const checkTizen = (env) => {
200
- const tizenPath = tizenCli(env.tizenCli);
201
- const sdbPath = sdb(env.sdb);
202
- const devices = listSdbDevices(sdbPath);
203
- console.log(`Tizen CLI: ${tizenPath}`);
204
- console.log(`sdb: ${sdbPath}`);
697
+ const checkTizen = Effect.fn("checkTizen")(function* (env) {
698
+ const tizenPath = yield* resolveTizenCli(env);
699
+ const sdbPath = yield* resolveSdb(env);
700
+ const devices = yield* listSdbDevices(sdbPath);
701
+ yield* Console.log(`Tizen CLI: ${tizenPath}`);
702
+ yield* Console.log(`sdb: ${sdbPath}`);
205
703
  if (devices.length === 0) {
206
- console.log("connected targets: none");
704
+ yield* Console.log("connected targets: none");
207
705
  return;
208
706
  }
209
- console.log("connected targets:");
210
- for (const device of devices) console.log(`- ${device.id}${device.label ? ` (${device.label})` : ""}`);
211
- };
212
- const createProfile = async ({ config, env }) => {
213
- const password = await readPassword(env.certPassword, "Tizen certificate password: ");
214
- if (!password) fail("TAIZN_CERT_PASSWORD is required to create the signing profile.");
215
- const certificates = getCertificates(config);
216
- const distributorPassword = env.distPassword || password;
217
- run(tizenCli(env.tizenCli), [
707
+ yield* Console.log("connected targets:");
708
+ for (const device of devices) yield* Console.log(`- ${device.id}${device.label ? ` (${device.label})` : ""}`);
709
+ });
710
+ const createProfile = Effect.fn("createProfile")(function* ({ config, env }) {
711
+ const password = yield* readPassword(env.certPassword, "Tizen certificate password: ");
712
+ if (!password) return yield* MissingPassword.make({
713
+ action: "create the signing profile",
714
+ variable: "TAIZN_CERT_PASSWORD"
715
+ });
716
+ const certificates = yield* getCertificates(config);
717
+ const distributorPassword = env.distPassword ?? password;
718
+ yield* run$1(yield* resolveTizenCli(env), [
218
719
  "security-profiles",
219
720
  "add",
220
721
  "-f",
@@ -229,43 +730,102 @@ const createProfile = async ({ config, env }) => {
229
730
  certificates.distributor,
230
731
  "-dp",
231
732
  distributorPassword
232
- ]);
233
- console.log(`Configured active Tizen signing profile: ${config.signing.profile}`);
234
- };
235
- const packageWidget = ({ config, env }) => {
733
+ ], { env: yield* baseChildEnv() });
734
+ yield* Console.log(`Configured active Tizen signing profile: ${config.signing.profile}`);
735
+ });
736
+ const packageWidget = Effect.fn("packageWidget")(function* ({ config, env }) {
236
737
  const [command, ...args] = config.build.command;
738
+ const tizenPath = yield* resolveTizenCli(env);
739
+ const paths = yield* getPaths();
237
740
  const variant = getVariant(config, env.variant);
238
- run(command, args, { env: appBuildEnv() });
239
- stageWidget(config, variant);
240
- run(tizenCli(env.tizenCli), [
741
+ yield* run$1(command, args, { env: yield* appBuildEnv() });
742
+ yield* stageWidget(config, variant);
743
+ yield* run$1(tizenPath, [
241
744
  "package",
242
745
  "-t",
243
746
  "wgt",
244
747
  "-s",
245
748
  config.signing.profile,
246
749
  "-o",
247
- outputDir,
750
+ paths.outputDir,
248
751
  "--",
249
- stageDir
250
- ]);
251
- const built = findBuiltWidget();
252
- const installable = join(outputDir, `${variant.bundleName}.wgt`);
253
- if (built !== installable) copyFileSync(built, installable);
254
- console.log(`Packaged ${installable}`);
752
+ paths.stageDir
753
+ ], { env: yield* baseChildEnv() });
754
+ const built = yield* findBuiltWidget();
755
+ const installable = join(paths.outputDir, `${variant.bundleName}.wgt`);
756
+ if (built !== installable) yield* (yield* FileSystem.FileSystem).copyFile(built, installable).pipe(Effect.mapError((cause) => FileSystemFailure.make({
757
+ cause,
758
+ operation: "copy",
759
+ path: installable
760
+ })));
761
+ yield* Console.log(`Packaged ${installable}`);
255
762
  return installable;
256
- };
257
- const installWidget = (context) => {
258
- const built = packageWidget(context);
259
- const target = resolveInstallTarget(context.env);
260
- if (context.env.target) run(sdb(context.env.sdb), ["connect", context.env.target]);
261
- const installArgs = [
763
+ });
764
+ const installWidget = Effect.fn("installWidget")(function* (context) {
765
+ const built = yield* packageWidget(context);
766
+ const target = yield* resolveInstallTarget(context.env);
767
+ const sdbPath = yield* resolveSdb(context.env);
768
+ const tizenPath = yield* resolveTizenCli(context.env);
769
+ if (context.env.target) yield* run$1(sdbPath, ["connect", context.env.target], { env: yield* baseChildEnv() });
770
+ yield* run$1(tizenPath, target ? [
771
+ "install",
772
+ "-n",
773
+ built,
774
+ "-s",
775
+ target
776
+ ] : [
262
777
  "install",
263
778
  "-n",
264
779
  built
265
- ];
266
- if (target) installArgs.push("-s", target);
267
- run(tizenCli(context.env.tizenCli), installArgs);
268
- };
780
+ ], { env: yield* baseChildEnv() });
781
+ });
782
+ const runWidget = Effect.fn("runWidget")(function* ({ config, env }) {
783
+ const variant = getVariant(config, env.variant);
784
+ const sdbPath = yield* resolveSdb(env);
785
+ const tizenPath = yield* resolveTizenCli(env);
786
+ if (env.target) yield* run$1(sdbPath, ["connect", env.target], { env: yield* baseChildEnv() });
787
+ const target = yield* resolveRunTarget(env, sdbPath);
788
+ yield* run$1(tizenPath, target ? [
789
+ "run",
790
+ "-p",
791
+ variant.applicationId,
792
+ target.flag,
793
+ target.value
794
+ ] : [
795
+ "run",
796
+ "-p",
797
+ variant.applicationId
798
+ ], { env: yield* baseChildEnv() });
799
+ yield* Console.log(`Launched ${variant.applicationId}`);
800
+ });
801
+ const listInstalledApplications = Effect.fn("listInstalledApplications")(function* (env, query) {
802
+ const sdbPath = yield* resolveSdb(env);
803
+ if (env.target) yield* run$1(sdbPath, ["connect", env.target], { env: yield* baseChildEnv() });
804
+ const target = yield* resolveRequiredSdbTarget(env, sdbPath);
805
+ const output = yield* capture(sdbPath, [
806
+ "-s",
807
+ target,
808
+ "shell",
809
+ "0",
810
+ "applist"
811
+ ]);
812
+ const queryLabel = query?.trim();
813
+ const normalizedQuery = normalizeQuery(queryLabel);
814
+ const applications = parseInstalledApplications(output).filter((application) => matchesApplicationQuery(application, normalizedQuery));
815
+ const suffix = queryLabel ? ` matching "${queryLabel}"` : "";
816
+ yield* Console.log(`Installed Tizen applications${suffix} on ${target}:`);
817
+ if (applications.length === 0) {
818
+ yield* Console.log("none");
819
+ return;
820
+ }
821
+ for (const application of applications) yield* Console.log(`- ${application.name} (${application.applicationId})`);
822
+ });
823
+ const resolveTizenCli = Effect.fn("resolveTizenCli")(function* (env) {
824
+ return yield* requireFile(env.tizenCli ?? (yield* defaultTizenCli()), "Tizen CLI");
825
+ });
826
+ const resolveSdb = Effect.fn("resolveSdb")(function* (env) {
827
+ return yield* requireFile(env.sdb ?? (yield* defaultSdb()), "sdb");
828
+ });
269
829
  const getVariant = (config, variant) => config.widget.variants[variant];
270
830
  const getWidgetStageOptions = (config, variant) => ({
271
831
  excludeFiles: [...config.widget.excludeFiles ?? [], ...variant.excludeFiles ?? []],
@@ -273,82 +833,133 @@ const getWidgetStageOptions = (config, variant) => ({
273
833
  injectWebapis: variant.injectWebapis ?? config.widget.injectWebapis,
274
834
  rewriteAssetUrls: variant.rewriteAssetUrls ?? config.widget.rewriteAssetUrls
275
835
  });
276
- const getCertificates = (config) => {
277
- const certificatesDir = appPath(config.signing.certificateDir);
836
+ const getCertificates = Effect.fn("getCertificates")(function* (config) {
837
+ const certificatesDir = appPath((yield* getPaths()).appDir, config.signing.certificateDir);
278
838
  return {
279
- author: requireFile(join(certificatesDir, "author.p12"), "Author certificate"),
280
- distributor: requireFile(join(certificatesDir, "distributor.p12"), "Distributor certificate")
839
+ author: yield* requireFile(join(certificatesDir, "author.p12"), "Author certificate"),
840
+ distributor: yield* requireFile(join(certificatesDir, "distributor.p12"), "Distributor certificate")
281
841
  };
282
- };
283
- const rewriteConfigForWidget = (variant) => {
284
- const targetPath = join(stageDir, "config.xml");
285
- let widgetConfig = readFileSync(targetPath, "utf8");
286
- widgetConfig = widgetConfig.replace(/<tizen:application\b[^>]*\/>/, (tag) => setXmlAttribute(setXmlAttribute(tag, "id", variant.applicationId), "package", variant.packageId));
287
- widgetConfig = widgetConfig.replace(/<name>[^<]*<\/name>/, `<name>${escapeXml(variant.name)}</name>`);
288
- writeFileSync(targetPath, widgetConfig);
289
- };
290
- const rewriteIndexForWidget = (options) => {
291
- const targetPath = join(stageDir, "index.html");
842
+ });
843
+ const rewriteConfigForWidget = Effect.fn("rewriteConfigForWidget")(function* (variant) {
844
+ const fs = yield* FileSystem.FileSystem;
845
+ const targetPath = join((yield* getPaths()).stageDir, "config.xml");
846
+ const rewritten = (yield* fs.readFileString(targetPath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
847
+ cause,
848
+ operation: "read",
849
+ path: targetPath
850
+ })))).replace(/<tizen:application\b[^>]*\/>/, (tag) => setXmlAttribute(setXmlAttribute(tag, "id", variant.applicationId), "package", variant.packageId)).replace(/<name>[^<]*<\/name>/, `<name>${escapeXml(variant.name)}</name>`);
851
+ yield* fs.writeFileString(targetPath, rewritten).pipe(Effect.mapError((cause) => FileSystemFailure.make({
852
+ cause,
853
+ operation: "write",
854
+ path: targetPath
855
+ })));
856
+ });
857
+ const rewriteIndexForWidget = Effect.fn("rewriteIndexForWidget")(function* (options) {
858
+ const fs = yield* FileSystem.FileSystem;
859
+ const paths = yield* getPaths();
860
+ const targetPath = join(paths.stageDir, "index.html");
292
861
  const webapisScript = "<script src=\"$WEBAPIS/webapis/webapis.js\"><\/script>";
293
- let html = readFileSync(appPath(options.indexHtml), "utf8");
294
- if (options.rewriteAssetUrls) html = html.replaceAll("href=\"/", "href=\"./").replaceAll("src=\"/", "src=\"./");
295
- if (options.injectWebapis !== false && !html.includes("$WEBAPIS/webapis/webapis.js")) html = html.replace("</head>", `${webapisScript}</head>`);
296
- writeFileSync(targetPath, html);
297
- };
298
- const assertBuildOutput = (config) => {
299
- const sourceDir = appPath(config.build.output);
300
- requireFile(sourceDir, "Tizen build output");
301
- for (const requiredFile of config.build.requiredFiles || []) requireFile(join(sourceDir, requiredFile), `Tizen build output ${requiredFile}`);
862
+ const indexPath = appPath(paths.appDir, options.indexHtml);
863
+ const source = yield* fs.readFileString(indexPath).pipe(Effect.mapError((cause) => FileSystemFailure.make({
864
+ cause,
865
+ operation: "read",
866
+ path: indexPath
867
+ })));
868
+ const withAssets = options.rewriteAssetUrls ? source.replaceAll("href=\"/", "href=\"./").replaceAll("src=\"/", "src=\"./") : source;
869
+ const html = options.injectWebapis !== false && !withAssets.includes("$WEBAPIS/webapis/webapis.js") ? withAssets.replace("</head>", `${webapisScript}</head>`) : withAssets;
870
+ yield* fs.writeFileString(targetPath, html).pipe(Effect.mapError((cause) => FileSystemFailure.make({
871
+ cause,
872
+ operation: "write",
873
+ path: targetPath
874
+ })));
875
+ });
876
+ const assertBuildOutput = Effect.fn("assertBuildOutput")(function* (config) {
877
+ const sourceDir = appPath((yield* getPaths()).appDir, config.build.output);
878
+ yield* requireFile(sourceDir, "Tizen build output");
879
+ for (const requiredFile of config.build.requiredFiles ?? []) yield* requireFile(join(sourceDir, requiredFile), `Tizen build output ${requiredFile}`);
302
880
  return sourceDir;
303
- };
304
- const removeExcludedStageFiles = (excludeFiles) => {
305
- for (const file of excludeFiles) rmSync(join(stageDir, file), {
306
- force: true,
307
- recursive: true
308
- });
309
- };
310
- const stageWidget = (config, variant) => {
311
- const sourceDir = assertBuildOutput(config);
881
+ });
882
+ const removeExcludedStageFiles = Effect.fn("removeExcludedStageFiles")(function* (excludeFiles) {
883
+ const fs = yield* FileSystem.FileSystem;
884
+ const paths = yield* getPaths();
885
+ for (const file of excludeFiles) {
886
+ const path = join(paths.stageDir, file);
887
+ yield* fs.remove(path, {
888
+ force: true,
889
+ recursive: true
890
+ }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
891
+ cause,
892
+ operation: "remove",
893
+ path
894
+ })));
895
+ }
896
+ });
897
+ const stageWidget = Effect.fn("stageWidget")(function* (config, variant) {
898
+ const fs = yield* FileSystem.FileSystem;
899
+ const paths = yield* getPaths();
900
+ const sourceDir = yield* assertBuildOutput(config);
312
901
  const options = getWidgetStageOptions(config, variant);
313
- rmSync(stageDir, {
902
+ const configXml = appPath(paths.appDir, config.widget.configXml);
903
+ const icon = yield* requireFile(appPath(paths.appDir, variant.icon), "Tizen widget icon");
904
+ yield* fs.remove(paths.stageDir, {
314
905
  force: true,
315
906
  recursive: true
316
- });
317
- rmSync(outputDir, {
907
+ }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
908
+ cause,
909
+ operation: "remove",
910
+ path: paths.stageDir
911
+ })));
912
+ yield* fs.remove(paths.outputDir, {
318
913
  force: true,
319
914
  recursive: true
320
- });
321
- mkdirSync(stageDir, { recursive: true });
322
- mkdirSync(outputDir, { recursive: true });
323
- cpSync(sourceDir, stageDir, { recursive: true });
324
- copyFileSync(appPath(config.widget.configXml), join(stageDir, "config.xml"));
325
- copyFileSync(requireFile(appPath(variant.icon), "Tizen widget icon"), join(stageDir, "icon.png"));
326
- removeExcludedStageFiles(options.excludeFiles);
327
- rewriteConfigForWidget(variant);
328
- rewriteIndexForWidget(options);
329
- };
330
- const findBuiltWidget = () => {
331
- const built = execFileSync("find", [
332
- outputDir,
333
- "-maxdepth",
334
- "1",
335
- "-name",
336
- "*.wgt",
337
- "-print"
338
- ], { encoding: "utf8" }).trim().split("\n").filter(Boolean).at(0);
339
- if (typeof built === "string") return built;
340
- return fail(`No .wgt package was produced in ${outputDir}`);
341
- };
342
- const listSdbDevices = (sdbPath) => {
343
- return execFileSync(sdb(sdbPath), ["devices"], {
344
- cwd: appDir,
345
- encoding: "utf8",
346
- env: {
347
- ...baseChildEnv(),
348
- PATH: `${join(homedir(), "tizen-studio/tools")}:${process.env.PATH || ""}`
349
- }
350
- }).split("\n").slice(1).map((line) => line.trim()).filter(Boolean).map(parseSdbDevice).filter((device) => device.id && device.state === "device");
351
- };
915
+ }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
916
+ cause,
917
+ operation: "remove",
918
+ path: paths.outputDir
919
+ })));
920
+ yield* fs.makeDirectory(paths.stageDir, { recursive: true }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
921
+ cause,
922
+ operation: "mkdir",
923
+ path: paths.stageDir
924
+ })));
925
+ yield* fs.makeDirectory(paths.outputDir, { recursive: true }).pipe(Effect.mapError((cause) => FileSystemFailure.make({
926
+ cause,
927
+ operation: "mkdir",
928
+ path: paths.outputDir
929
+ })));
930
+ yield* fs.copy(sourceDir, paths.stageDir).pipe(Effect.mapError((cause) => FileSystemFailure.make({
931
+ cause,
932
+ operation: "copy",
933
+ path: paths.stageDir
934
+ })));
935
+ yield* fs.copyFile(configXml, join(paths.stageDir, "config.xml")).pipe(Effect.mapError((cause) => FileSystemFailure.make({
936
+ cause,
937
+ operation: "copy",
938
+ path: configXml
939
+ })));
940
+ yield* fs.copyFile(icon, join(paths.stageDir, "icon.png")).pipe(Effect.mapError((cause) => FileSystemFailure.make({
941
+ cause,
942
+ operation: "copy",
943
+ path: icon
944
+ })));
945
+ yield* removeExcludedStageFiles(options.excludeFiles);
946
+ yield* rewriteConfigForWidget(variant);
947
+ yield* rewriteIndexForWidget(options);
948
+ });
949
+ const findBuiltWidget = Effect.fn("findBuiltWidget")(function* () {
950
+ const fs = yield* FileSystem.FileSystem;
951
+ const paths = yield* getPaths();
952
+ const built = (yield* fs.readDirectory(paths.outputDir).pipe(Effect.mapError((cause) => FileSystemFailure.make({
953
+ cause,
954
+ operation: "readDirectory",
955
+ path: paths.outputDir
956
+ })))).filter((entry) => entry.endsWith(".wgt")).map((entry) => join(paths.outputDir, entry)).at(0);
957
+ if (built) return built;
958
+ return yield* PackageNotProduced.make({ outputDir: paths.outputDir });
959
+ });
960
+ const listSdbDevices = Effect.fn("listSdbDevices")(function* (sdbPath) {
961
+ return (yield* capture(sdbPath, ["devices"])).split("\n").slice(1).map((line) => line.trim()).filter(Boolean).map(parseSdbDevice).filter((device) => device.id && device.state === "device");
962
+ });
352
963
  const parseSdbDevice = (line) => {
353
964
  const [id = "", state = "", label = ""] = line.split(/\s+/, 3);
354
965
  return {
@@ -357,61 +968,177 @@ const parseSdbDevice = (line) => {
357
968
  state
358
969
  };
359
970
  };
360
- const resolveInstallTarget = (env) => {
971
+ const resolveInstallTarget = Effect.fn("resolveInstallTarget")(function* (env) {
361
972
  if (env.target) return env.target;
362
- const devices = listSdbDevices(env.sdb);
973
+ const devices = yield* listSdbDevices(yield* resolveSdb(env));
363
974
  if (devices.length === 1) {
364
- const [device] = devices;
365
- console.log(`Using connected Tizen target: ${device.id}${device.label ? ` (${device.label})` : ""}`);
366
- return device.id;
975
+ const device = devices[0];
976
+ if (device) {
977
+ yield* Console.log(`Using connected Tizen target: ${device.id}${device.label ? ` (${device.label})` : ""}`);
978
+ return device.id;
979
+ }
367
980
  }
368
- if (devices.length > 1) fail(`Multiple Tizen targets are connected: ${devices.map((device) => device.id).join(", ")}. Set TAIZN_TARGET explicitly.`);
369
- return null;
370
- };
981
+ if (devices.length > 1) return yield* MultipleTargetsConnected.make({ targets: devices.map((device) => device.id) });
982
+ });
983
+ const resolveRunTarget = Effect.fn("resolveRunTarget")(function* (env, sdbPath) {
984
+ if (env.target) return {
985
+ flag: "-s",
986
+ value: env.target
987
+ };
988
+ const devices = yield* listSdbDevices(sdbPath);
989
+ if (devices.length === 1) {
990
+ const device = devices[0];
991
+ if (device) {
992
+ yield* Console.log(`Using connected Tizen target: ${device.id}${device.label ? ` (${device.label})` : ""}`);
993
+ return {
994
+ flag: "-s",
995
+ value: device.id
996
+ };
997
+ }
998
+ }
999
+ if (devices.length > 1) return yield* MultipleTargetsConnected.make({ targets: devices.map((device) => device.id) });
1000
+ });
1001
+ const resolveRequiredSdbTarget = Effect.fn("resolveRequiredSdbTarget")(function* (env, sdbPath) {
1002
+ if (env.target) return env.target;
1003
+ const devices = yield* listSdbDevices(sdbPath);
1004
+ if (devices.length === 1) {
1005
+ const device = devices[0];
1006
+ if (device) {
1007
+ yield* Console.log(`Using connected Tizen target: ${device.id}${device.label ? ` (${device.label})` : ""}`);
1008
+ return device.id;
1009
+ }
1010
+ }
1011
+ if (devices.length > 1) return yield* MultipleTargetsConnected.make({ targets: devices.map((device) => device.id) });
1012
+ return yield* MissingTizenTarget.make({});
1013
+ });
1014
+ const parseInstalledApplications = (output) => output.split("\n").flatMap((line) => {
1015
+ const match = line.match(/^\s*'([^']*)'\s+'([^']*)'\s*$/);
1016
+ const name = match?.[1]?.trim();
1017
+ const applicationId = match?.[2]?.trim();
1018
+ return name && applicationId ? [{
1019
+ applicationId,
1020
+ name
1021
+ }] : [];
1022
+ });
1023
+ const normalizeQuery = (query) => query?.trim().toLowerCase();
1024
+ const matchesApplicationQuery = (application, normalizedQuery) => !normalizedQuery || application.name.toLowerCase().includes(normalizedQuery) || application.applicationId.toLowerCase().includes(normalizedQuery);
1025
+ const run$1 = Effect.fn("run")(function* (command, args, options) {
1026
+ const paths = yield* getPaths();
1027
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1028
+ const env = yield* withTizenPath(options.env ?? (yield* baseChildEnv()));
1029
+ if ((yield* spawner.exitCode(ChildProcess.make(command, args, {
1030
+ cwd: options.cwd ?? paths.appDir,
1031
+ env,
1032
+ stderr: "inherit",
1033
+ stdin: "inherit",
1034
+ stdout: "inherit"
1035
+ })).pipe(Effect.mapError(() => CommandFailed.make({
1036
+ args: redactCommandArgs(args),
1037
+ command
1038
+ })))) !== 0) return yield* CommandFailed.make({
1039
+ args: redactCommandArgs(args),
1040
+ command
1041
+ });
1042
+ });
1043
+ const capture = Effect.fn("capture")(function* (command, args) {
1044
+ const output = yield* Effect.scoped(Effect.gen(function* () {
1045
+ const paths = yield* getPaths();
1046
+ const env = yield* withTizenPath(yield* baseChildEnv());
1047
+ const process = yield* ChildProcess.make(command, args, {
1048
+ cwd: paths.appDir,
1049
+ env,
1050
+ stderr: "inherit"
1051
+ }).pipe(Effect.mapError(() => CommandFailed.make({
1052
+ args: redactCommandArgs(args),
1053
+ command
1054
+ })));
1055
+ const output = yield* process.stdout.pipe(Stream.decodeText, Stream.mkString).pipe(Effect.mapError(() => CommandFailed.make({
1056
+ args: redactCommandArgs(args),
1057
+ command
1058
+ })));
1059
+ return {
1060
+ exitCode: yield* process.exitCode.pipe(Effect.mapError(() => CommandFailed.make({
1061
+ args: redactCommandArgs(args),
1062
+ command
1063
+ }))),
1064
+ output
1065
+ };
1066
+ }));
1067
+ if (output.exitCode !== 0) return yield* CommandFailed.make({
1068
+ args: redactCommandArgs(args),
1069
+ command
1070
+ });
1071
+ return output.output;
1072
+ });
371
1073
  //#endregion
372
1074
  //#region src/cli.ts
373
- const runSync = (operation) => Effect.sync(() => {
374
- operation(loadContext());
375
- });
376
- const runEnvSync = (operation) => Effect.sync(() => {
377
- operation(loadEnv());
1075
+ const withContext = (operation) => Effect.gen(function* () {
1076
+ yield* operation(yield* loadContext());
378
1077
  });
379
- const taizn = Command.make("taizn", {}, () => runSync((context) => {
380
- packageWidget(context);
1078
+ const taizn = Command.make("taizn", {}, () => withContext((context) => packageWidget(context).pipe(Effect.asVoid)));
1079
+ const check = Command.make("check", {}, () => Effect.gen(function* () {
1080
+ yield* checkTizen(yield* loadEnv());
381
1081
  }));
382
- const check = Command.make("check", {}, () => runEnvSync((env) => {
383
- checkTizen(env);
1082
+ const apps = Command.make("apps", { query: Argument.string("query").pipe(Argument.optional) }, ({ query }) => Effect.gen(function* () {
1083
+ yield* listInstalledApplications(yield* loadEnv(), Option.getOrUndefined(query));
384
1084
  }));
385
- const profile = Command.make("profile", {}, () => Effect.promise(async () => {
386
- await createProfile(loadContext());
1085
+ const profile = Command.make("profile", {}, () => withContext((context) => createProfile(context)));
1086
+ const pack = Command.make("package", {}, () => withContext((context) => packageWidget(context).pipe(Effect.asVoid)));
1087
+ const install = Command.make("install", {}, () => withContext((context) => installWidget(context)));
1088
+ const run = Command.make("run", {}, () => withContext((context) => runWidget(context)));
1089
+ const tvPair = Command.make("pair", {}, () => Effect.gen(function* () {
1090
+ yield* pairSamsungTvRemote(yield* loadEnv());
387
1091
  }));
388
- const pack = Command.make("package", {}, () => runSync((context) => {
389
- packageWidget(context);
1092
+ const tvPress = Command.make("press", {
1093
+ delayMs: Flag.integer("delay-ms").pipe(Flag.withDefault(250)),
1094
+ keys: Argument.string("key").pipe(Argument.variadic({ min: 1 }))
1095
+ }, ({ delayMs, keys }) => Effect.gen(function* () {
1096
+ yield* sendSamsungTvKeys(yield* loadEnv(), keys, { delayMs });
390
1097
  }));
391
- const install = Command.make("install", {}, () => runSync((context) => {
392
- installWidget(context);
1098
+ const tvInfo = Command.make("info", {}, () => Effect.gen(function* () {
1099
+ yield* showSamsungTvInfo(yield* loadEnv());
393
1100
  }));
1101
+ const tv = Command.make("tv", {}).pipe(Command.withSubcommands([
1102
+ tvPair,
1103
+ tvPress,
1104
+ tvInfo
1105
+ ]));
394
1106
  const command = taizn.pipe(Command.withSubcommands([
1107
+ apps,
395
1108
  check,
396
1109
  profile,
397
1110
  pack,
398
- install
1111
+ install,
1112
+ run,
1113
+ tv
399
1114
  ]));
400
1115
  //#endregion
401
1116
  //#region src/taizn.ts
402
- const PackageJsonSchema = Schema.Struct({ version: Schema.String });
403
- loadLocalEnv();
404
- Command.run(command, {
405
- name: "taizn",
406
- version: getPackageVersion()
407
- })(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
408
- function getPackageVersion() {
409
- try {
410
- return Schema.decodeUnknownSync(Schema.parseJson(PackageJsonSchema))(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
411
- } catch (error) {
412
- if (ParseResult.isParseError(error)) return "0.0.0";
413
- return "0.0.0";
1117
+ var PackageJson = class extends Schema.Class("PackageJson")({ version: Schema.String }) {};
1118
+ const appLayer = Layer.mergeAll(NodeServices.layer, TaiznSystem.Live);
1119
+ const getPackageVersion = Effect.fn("getPackageVersion")(function* () {
1120
+ const source = yield* (yield* FileSystem.FileSystem).readFileString(fileURLToPath(new URL("../package.json", import.meta.url)));
1121
+ const json = yield* Effect.try({
1122
+ try: () => JSON.parse(source),
1123
+ catch: () => void 0
1124
+ });
1125
+ return (yield* Schema.decodeUnknownEffect(PackageJson)(json).pipe(Effect.catch(() => Effect.succeed(PackageJson.make({ version: "0.0.0" }))))).version;
1126
+ }, Effect.catch(() => Effect.succeed("0.0.0")));
1127
+ const program = Effect.gen(function* () {
1128
+ yield* loadLocalEnv();
1129
+ const version = yield* getPackageVersion();
1130
+ yield* Command.run(command, { version });
1131
+ }).pipe(Effect.catch(handleMainError), Effect.provide(appLayer));
1132
+ NodeRuntime.runMain(program);
1133
+ function handleMainError(error) {
1134
+ if (CliError.isCliError(error)) {
1135
+ if (error._tag === "ShowHelp" && error.errors.length === 0) return Effect.void;
1136
+ return markFailed;
414
1137
  }
1138
+ return Console.error(renderError(error)).pipe(Effect.andThen(markFailed));
415
1139
  }
1140
+ const markFailed = Effect.sync(() => {
1141
+ process.exitCode = 1;
1142
+ });
416
1143
  //#endregion
417
1144
  export {};