@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/README.md +30 -1
- package/dist/taizn.mjs +987 -260
- package/docs/DISTRIBUTION.md +54 -0
- package/docs/TV_REMOTE.md +70 -0
- package/package.json +10 -9
package/dist/taizn.mjs
CHANGED
|
@@ -1,72 +1,207 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Effect } from "effect";
|
|
5
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
27
|
-
|
|
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 = { ...
|
|
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
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
223
|
+
reject(/* @__PURE__ */ new Error("interrupted"));
|
|
224
|
+
return;
|
|
83
225
|
}
|
|
84
226
|
if (char === "\r" || char === "\n") {
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
}).
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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(""", "\"").replaceAll("&", "&");
|
|
188
686
|
//#endregion
|
|
189
687
|
//#region src/xml.ts
|
|
190
688
|
const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -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 =
|
|
201
|
-
const sdbPath =
|
|
202
|
-
const devices = listSdbDevices(sdbPath);
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
704
|
+
yield* Console.log("connected targets: none");
|
|
207
705
|
return;
|
|
208
706
|
}
|
|
209
|
-
|
|
210
|
-
for (const device of devices)
|
|
211
|
-
};
|
|
212
|
-
const createProfile =
|
|
213
|
-
const password =
|
|
214
|
-
if (!password)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
const
|
|
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
|
-
|
|
267
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
973
|
+
const devices = yield* listSdbDevices(yield* resolveSdb(env));
|
|
363
974
|
if (devices.length === 1) {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
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)
|
|
369
|
-
|
|
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
|
|
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", {}, () =>
|
|
380
|
-
|
|
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
|
|
383
|
-
|
|
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", {}, () =>
|
|
386
|
-
|
|
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
|
|
389
|
-
|
|
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
|
|
392
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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 {};
|