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