@swipt/cli 0.1.0-nightly.31 → 0.1.0-nightly.33
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/client.mjs +312 -0
- package/device.mjs +620 -0
- package/ignore.mjs +137 -0
- package/output.mjs +61 -0
- package/package.json +12 -5
- package/{cli.mjs → project.mjs} +109 -111
- package/proto/common_pb.js +98 -0
- package/proto/logs_pb.js +34 -0
- package/proto/repl_pb.js +55 -0
- package/proto/swiplets_pb.js +179 -0
- package/proto/system_pb.js +27 -0
- package/sourcemap.mjs +77 -0
- package/swipt.mjs +301 -0
package/device.mjs
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
|
|
8
|
+
import { fromJson, toJson } from "@bufbuild/protobuf";
|
|
9
|
+
import { ValueSchema, timestampFromMs } from "@bufbuild/protobuf/wkt";
|
|
10
|
+
import Bonjour from "bonjour-service";
|
|
11
|
+
|
|
12
|
+
import { UsageError, open, unauthenticated } from "./client.mjs";
|
|
13
|
+
import { Ignorer, relativePath } from "./ignore.mjs";
|
|
14
|
+
import { columns, dateTime, enumText, printLog, printScriptError, serviceText } from "./output.mjs";
|
|
15
|
+
import { buildProject, loadProject } from "./project.mjs";
|
|
16
|
+
import { DeviceConnectionProblem, DeviceConnectionProblemSchema, DeviceConnectionStateSchema, LogLevel } from "./proto/common_pb.js";
|
|
17
|
+
import { ServiceStatus, ServiceStatusSchema } from "./proto/swiplets_pb.js";
|
|
18
|
+
import { sources } from "./sourcemap.mjs";
|
|
19
|
+
|
|
20
|
+
const uploadChunk = 256 << 10;
|
|
21
|
+
const browseLimit = 1500;
|
|
22
|
+
const rebuildDelay = 300;
|
|
23
|
+
const logReconnect = 2000;
|
|
24
|
+
const scriptGrace = 10_000;
|
|
25
|
+
|
|
26
|
+
const { version } = createRequire(import.meta.url)("./package.json");
|
|
27
|
+
|
|
28
|
+
const readStdin = () => fs.readFileSync(0);
|
|
29
|
+
|
|
30
|
+
const indented = value => JSON.stringify(value, null, 2).replaceAll("\n", "\n ");
|
|
31
|
+
|
|
32
|
+
function elapsed(since) {
|
|
33
|
+
const ms = Date.now() - since;
|
|
34
|
+
if (ms < 1000) {
|
|
35
|
+
return `${ms}ms`;
|
|
36
|
+
}
|
|
37
|
+
const seconds = ms / 1000;
|
|
38
|
+
if (seconds < 60) {
|
|
39
|
+
return `${Number(seconds.toFixed(3))}s`;
|
|
40
|
+
}
|
|
41
|
+
return `${Math.floor(seconds / 60)}m${Number((seconds % 60).toFixed(3))}s`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const session = (context, run) => run(open(context.globals));
|
|
45
|
+
|
|
46
|
+
export async function devices({ signal }) {
|
|
47
|
+
const bonjour = new Bonjour();
|
|
48
|
+
const found = new Map();
|
|
49
|
+
try {
|
|
50
|
+
const browser = bonjour.find({ type: "swipt" }, service => {
|
|
51
|
+
if (service.host && !found.has(service.name)) {
|
|
52
|
+
found.set(service.name, { name: service.name, host: service.host.replace(/\.$/, ""), port: service.port });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
await sleep(browseLimit, undefined, { signal });
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
browser.stop();
|
|
61
|
+
} finally {
|
|
62
|
+
bonjour.destroy();
|
|
63
|
+
}
|
|
64
|
+
if (found.size === 0) {
|
|
65
|
+
console.error("no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const hostPort = service => (service.host.includes(":") ? `[${service.host}]:${service.port}` : `${service.host}:${service.port}`);
|
|
69
|
+
console.log(columns([["NAME", "HOST"], ...[...found.values()].map(service => [service.name, `http://${hostPort(service)}`])]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function printVersion() {
|
|
73
|
+
console.log("swipt", version, `node ${process.versions.node}`, `${process.platform}/${process.arch}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const info = context =>
|
|
77
|
+
session(context, async client => {
|
|
78
|
+
const found = await client.system.getInfo({}, { signal: context.signal });
|
|
79
|
+
const connection = found.deviceConnection;
|
|
80
|
+
let state = enumText(DeviceConnectionStateSchema, connection?.state ?? 0, "DEVICE_CONNECTION_STATE_");
|
|
81
|
+
if (connection && connection.problem !== DeviceConnectionProblem.UNSPECIFIED) {
|
|
82
|
+
state += `, ${enumText(DeviceConnectionProblemSchema, connection.problem, "DEVICE_CONNECTION_PROBLEM_")}`;
|
|
83
|
+
}
|
|
84
|
+
if (connection?.message) {
|
|
85
|
+
state += `: ${connection.message}`;
|
|
86
|
+
}
|
|
87
|
+
console.log(
|
|
88
|
+
columns([
|
|
89
|
+
["device:", found.deviceName],
|
|
90
|
+
["udid:", found.udid],
|
|
91
|
+
["server:", found.serverVersion],
|
|
92
|
+
["connection:", state],
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const logs = context =>
|
|
98
|
+
session(context, async client => {
|
|
99
|
+
const { options } = context;
|
|
100
|
+
const levels = ["error", "warn", "info", "debug"];
|
|
101
|
+
const index = levels.indexOf(options.level);
|
|
102
|
+
if (index < 0) {
|
|
103
|
+
throw new UsageError("--level is error, warn, info or debug");
|
|
104
|
+
}
|
|
105
|
+
const request = { follow: options.follow, tail: options.tail ?? 100, swipletId: options.swiplet ?? "", level: index + 1 };
|
|
106
|
+
if (options.since > 0) {
|
|
107
|
+
request.since = timestampFromMs(Date.now() - options.since);
|
|
108
|
+
if (options.tail === undefined) {
|
|
109
|
+
request.tail = 0xffffffff;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for await (const response of client.logs.readLogs(request, { signal: context.signal })) {
|
|
113
|
+
printLog(response.entry, true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function readInput(value) {
|
|
118
|
+
if (value === undefined || value === "") {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
let data = value;
|
|
122
|
+
try {
|
|
123
|
+
if (value === "-") {
|
|
124
|
+
data = readStdin().toString();
|
|
125
|
+
} else if (value.startsWith("@")) {
|
|
126
|
+
data = fs.readFileSync(value.slice(1), "utf8");
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`read --input: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return fromJson(ValueSchema, JSON.parse(data));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new UsageError(`--input is not JSON: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const serviceStatus = state => (state.status === ServiceStatus.UNSPECIFIED ? "stopped" : enumText(ServiceStatusSchema, state.status, "SERVICE_STATUS_"));
|
|
139
|
+
|
|
140
|
+
const notInstalled = id => new Error(`there is no swiplet ${JSON.stringify(id)}`);
|
|
141
|
+
|
|
142
|
+
export const swiplets = context =>
|
|
143
|
+
session(context, async client => {
|
|
144
|
+
const { swiplets } = await client.swiplets.listSwiplets({}, { signal: context.signal });
|
|
145
|
+
if (context.args.length === 0) {
|
|
146
|
+
const rows = swiplets.map(swiplet => {
|
|
147
|
+
let service = "";
|
|
148
|
+
let status = "";
|
|
149
|
+
if (swiplet.service) {
|
|
150
|
+
service = swiplet.service.enabled ? "enabled" : "disabled";
|
|
151
|
+
status = serviceStatus(swiplet.service);
|
|
152
|
+
if (swiplet.service.error) {
|
|
153
|
+
status += `: ${swiplet.service.error.name}: ${swiplet.service.error.message}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [swiplet.id, swiplet.title, swiplet.version, swiplet.actions.map(action => action.name).join(","), service, status];
|
|
157
|
+
});
|
|
158
|
+
console.log(columns([["ID", "TITLE", "VERSION", "ACTIONS", "SERVICE", "STATUS"], ...rows]));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const swiplet = swiplets.find(it => it.id === context.args[0]);
|
|
162
|
+
if (!swiplet) {
|
|
163
|
+
throw notInstalled(context.args[0]);
|
|
164
|
+
}
|
|
165
|
+
console.log(swiplet.id, JSON.stringify(swiplet.title), swiplet.version);
|
|
166
|
+
if (swiplet.hasUi) {
|
|
167
|
+
console.log("ui: yes");
|
|
168
|
+
}
|
|
169
|
+
if (swiplet.service) {
|
|
170
|
+
console.log(`service: ${serviceText(swiplet.id, swiplet.service, sources.frame.bind(sources))}`);
|
|
171
|
+
}
|
|
172
|
+
for (const action of swiplet.actions) {
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(`action ${action.name} ${JSON.stringify(action.title)}${action.exposedToShortcuts ? " (shortcuts)" : ""}`);
|
|
175
|
+
if (action.description !== "") {
|
|
176
|
+
console.log(` ${action.description}`);
|
|
177
|
+
}
|
|
178
|
+
if (action.inputSchema) {
|
|
179
|
+
console.log(` input: ${indented(action.inputSchema)}`);
|
|
180
|
+
}
|
|
181
|
+
if (action.outputSchema) {
|
|
182
|
+
console.log(` output: ${indented(action.outputSchema)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const call = context =>
|
|
188
|
+
session(context, async client => {
|
|
189
|
+
const [id, action] = context.args;
|
|
190
|
+
const input = readInput(context.options.input);
|
|
191
|
+
let result;
|
|
192
|
+
for await (const response of client.swiplets.callAction({ swipletId: id, action, input }, { signal: context.signal })) {
|
|
193
|
+
if (response.part.case === "log") {
|
|
194
|
+
printLog(response.part.value, false);
|
|
195
|
+
} else if (response.part.case === "result") {
|
|
196
|
+
result = response.part.value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (result?.outcome.case === "output") {
|
|
200
|
+
console.log(JSON.stringify(toJson(ValueSchema, result.outcome.value), null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (result?.outcome.case === "error") {
|
|
204
|
+
throw printScriptError(result.outcome.value, sources.frame(id));
|
|
205
|
+
}
|
|
206
|
+
throw new Error("the server sent no result");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
async function watchServices(client, id, signal) {
|
|
210
|
+
for await (const response of client.swiplets.watchServices({ swipletId: id }, { signal })) {
|
|
211
|
+
console.log(`${response.swipletId}: ${serviceText(response.swipletId, response.service, sources.frame.bind(sources))}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const setService = enabled => context =>
|
|
216
|
+
session(context, async client => {
|
|
217
|
+
const id = context.args[0];
|
|
218
|
+
const response = await client.swiplets.setServiceEnabled({ swipletId: id, enabled }, { signal: context.signal });
|
|
219
|
+
if (context.options.watch) {
|
|
220
|
+
await watchServices(client, id, context.signal);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(`${id}: ${serviceText(id, response.service, sources.frame.bind(sources))}`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export const serviceWatch = context => session(context, client => watchServices(client, context.args[0] ?? "", context.signal));
|
|
227
|
+
|
|
228
|
+
export const remove = context =>
|
|
229
|
+
session(context, async client => {
|
|
230
|
+
await client.swiplets.removeSwiplet({ swipletId: context.args[0] }, { signal: context.signal });
|
|
231
|
+
console.log("removed", context.args[0]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export const filesList = context =>
|
|
235
|
+
session(context, async client => {
|
|
236
|
+
const [id, directory = ""] = context.args;
|
|
237
|
+
const { files } = await client.swiplets.listDataFiles({ swipletId: id, path: directory, recursive: context.options.recursive }, { signal: context.signal });
|
|
238
|
+
const rows = files.map(file => [file.directory ? "-" : String(file.size), dateTime(file.modified), file.directory ? `${file.path}/` : file.path]);
|
|
239
|
+
if (rows.length > 0) {
|
|
240
|
+
console.log(columns(rows));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const isLocal = name => {
|
|
245
|
+
const normalized = path.posix.normalize(name);
|
|
246
|
+
return !path.posix.isAbsolute(normalized) && normalized !== ".." && !normalized.startsWith("../");
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
class FileSink {
|
|
250
|
+
files = new Map();
|
|
251
|
+
saved = 0;
|
|
252
|
+
|
|
253
|
+
constructor(root) {
|
|
254
|
+
this.root = root;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
target(name) {
|
|
258
|
+
if (!isLocal(name)) {
|
|
259
|
+
throw new Error(`the server sent the path ${name}, which leaves the output directory`);
|
|
260
|
+
}
|
|
261
|
+
return path.join(this.root, ...name.split("/"));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
begin(name, directory) {
|
|
265
|
+
const target = this.target(name);
|
|
266
|
+
if (directory) {
|
|
267
|
+
fs.mkdirSync(target, { recursive: true });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
271
|
+
const previous = this.files.get(name);
|
|
272
|
+
if (previous !== undefined) {
|
|
273
|
+
fs.closeSync(previous);
|
|
274
|
+
}
|
|
275
|
+
this.files.set(name, fs.openSync(target, "w"));
|
|
276
|
+
this.saved++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
write(name, offset, data) {
|
|
280
|
+
const file = this.files.get(name);
|
|
281
|
+
if (file === undefined) {
|
|
282
|
+
throw new Error(`the server sent data for ${name} before announcing it`);
|
|
283
|
+
}
|
|
284
|
+
fs.writeSync(file, data, 0, data.length, Number(offset));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
close() {
|
|
288
|
+
for (const file of this.files.values()) {
|
|
289
|
+
fs.closeSync(file);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const printSaved = (target, count) => console.error(`saved ${count} file(s) to ${target}`);
|
|
295
|
+
|
|
296
|
+
export const filesRead = context =>
|
|
297
|
+
session(context, async client => {
|
|
298
|
+
const [id, given] = context.args;
|
|
299
|
+
const output = context.options.output ?? "";
|
|
300
|
+
const requested = path.posix.normalize(`/${given}`).replace(/^\/+|\/+$/g, "");
|
|
301
|
+
const printing = output === "" || output === "-";
|
|
302
|
+
const directory = () =>
|
|
303
|
+
new UsageError(
|
|
304
|
+
requested === ""
|
|
305
|
+
? `${JSON.stringify(given)} names the data directory of ${id}; save it with --output DIRECTORY`
|
|
306
|
+
: `${JSON.stringify(requested)} is a directory in the data directory of ${id}; save it with --output DIRECTORY`,
|
|
307
|
+
);
|
|
308
|
+
const relativeTo = name => name.slice(requested.length).replace(/^\//, "");
|
|
309
|
+
let sink;
|
|
310
|
+
let single;
|
|
311
|
+
const contents = [];
|
|
312
|
+
try {
|
|
313
|
+
for await (const response of client.swiplets.readDataFile({ swipletId: id, path: given }, { signal: context.signal })) {
|
|
314
|
+
if (response.part.case === "file") {
|
|
315
|
+
const file = response.part.value;
|
|
316
|
+
if (file.path === requested && !file.directory) {
|
|
317
|
+
single = file;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!sink) {
|
|
321
|
+
if (printing) {
|
|
322
|
+
throw directory();
|
|
323
|
+
}
|
|
324
|
+
sink = new FileSink(output);
|
|
325
|
+
}
|
|
326
|
+
sink.begin(relativeTo(file.path), file.directory);
|
|
327
|
+
} else if (response.part.case === "chunk") {
|
|
328
|
+
const chunk = response.part.value;
|
|
329
|
+
if (single && chunk.path === single.path) {
|
|
330
|
+
contents.push(chunk.data);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!sink) {
|
|
334
|
+
throw new Error(`the server sent data for ${chunk.path} before announcing it`);
|
|
335
|
+
}
|
|
336
|
+
sink.write(relativeTo(chunk.path), chunk.offset, chunk.data);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
sink?.close();
|
|
341
|
+
}
|
|
342
|
+
if (sink) {
|
|
343
|
+
printSaved(sink.root, sink.saved);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (!single) {
|
|
347
|
+
if (printing) {
|
|
348
|
+
throw directory();
|
|
349
|
+
}
|
|
350
|
+
fs.mkdirSync(output, { recursive: true });
|
|
351
|
+
printSaved(output, 0);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const data = Buffer.concat(contents);
|
|
355
|
+
if (printing) {
|
|
356
|
+
process.stdout.write(data);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let target = output;
|
|
360
|
+
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
|
|
361
|
+
target = path.join(target, path.posix.basename(single.path));
|
|
362
|
+
}
|
|
363
|
+
fs.writeFileSync(target, data);
|
|
364
|
+
printSaved(target, 1);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export const filesWrite = context =>
|
|
368
|
+
session(context, async client => {
|
|
369
|
+
const [id, name, file] = context.args;
|
|
370
|
+
const data = file === undefined || file === "-" ? readStdin() : fs.readFileSync(file);
|
|
371
|
+
await client.swiplets.writeDataFile(
|
|
372
|
+
(async function* () {
|
|
373
|
+
yield { part: { case: "header", value: { swipletId: id, path: name } } };
|
|
374
|
+
for (let offset = 0; offset < data.length; offset += uploadChunk) {
|
|
375
|
+
yield { part: { case: "data", value: data.subarray(offset, offset + uploadChunk) } };
|
|
376
|
+
}
|
|
377
|
+
})(),
|
|
378
|
+
{ signal: context.signal },
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
export const filesRemove = context =>
|
|
383
|
+
session(context, async client => {
|
|
384
|
+
const [id, name] = context.args;
|
|
385
|
+
await client.swiplets.removeDataFile({ swipletId: id, path: name, recursive: context.options.recursive }, { signal: context.signal });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
async function deploy(client, archive, signal) {
|
|
389
|
+
const data = fs.readFileSync(archive);
|
|
390
|
+
return client.swiplets.deploySwiplet(
|
|
391
|
+
(async function* () {
|
|
392
|
+
for (let offset = 0; offset < data.length || offset === 0; offset += uploadChunk) {
|
|
393
|
+
yield { chunk: data.subarray(offset, offset + uploadChunk) };
|
|
394
|
+
}
|
|
395
|
+
})(),
|
|
396
|
+
{ signal },
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const printDeployed = (deployed, toStdout) => (toStdout ? console.log : console.error)(deployed.replaced ? "replaced" : "installed", deployed.swiplet.id, deployed.swiplet.version);
|
|
401
|
+
|
|
402
|
+
export async function deployCommand(context) {
|
|
403
|
+
const [given] = context.args;
|
|
404
|
+
let archive;
|
|
405
|
+
if (given !== undefined && (given.endsWith(".swiplet") || given.endsWith(".zip"))) {
|
|
406
|
+
if (!fs.existsSync(given)) {
|
|
407
|
+
throw new Error(`there is no archive at ${given}`);
|
|
408
|
+
}
|
|
409
|
+
archive = given;
|
|
410
|
+
} else {
|
|
411
|
+
({ archive } = await buildProject(path.resolve(given ?? ".")));
|
|
412
|
+
}
|
|
413
|
+
await session(context, async client => printDeployed(await deploy(client, archive, context.signal), true));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function follow(signal, fatal, run) {
|
|
417
|
+
while (!signal.aborted) {
|
|
418
|
+
try {
|
|
419
|
+
await run();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (unauthenticated(error)) {
|
|
422
|
+
fatal(error);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await sleep(logReconnect, undefined, { signal });
|
|
428
|
+
} catch {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function develop(context) {
|
|
435
|
+
const { signal, printError } = context;
|
|
436
|
+
const project = loadProject(path.resolve(context.args[0] ?? "."));
|
|
437
|
+
const root = project.root;
|
|
438
|
+
sources.root = root;
|
|
439
|
+
const client = open(context.globals);
|
|
440
|
+
const controller = new AbortController();
|
|
441
|
+
const inner = AbortSignal.any([signal, controller.signal]);
|
|
442
|
+
let fatalReject;
|
|
443
|
+
const fatal = new Promise((_, reject) => {
|
|
444
|
+
fatalReject = reject;
|
|
445
|
+
});
|
|
446
|
+
follow(inner, fatalReject, async () => {
|
|
447
|
+
for await (const response of client.swiplets.watchServices({}, { signal: inner })) {
|
|
448
|
+
if (response.swipletId === project.config.id && response.service?.status === ServiceStatus.FAILED) {
|
|
449
|
+
console.error(`${response.swipletId}: ${serviceText(response.swipletId, response.service, sources.frame.bind(sources))}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
if (!context.options["no-logs"]) {
|
|
454
|
+
follow(inner, fatalReject, async () => {
|
|
455
|
+
for await (const response of client.logs.readLogs({ swipletId: project.config.id, follow: true }, { signal: inner })) {
|
|
456
|
+
printLog(response.entry, false);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
let deployed = "";
|
|
461
|
+
const cycle = async () => {
|
|
462
|
+
const started = Date.now();
|
|
463
|
+
let built;
|
|
464
|
+
try {
|
|
465
|
+
built = await buildProject(root);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (!inner.aborted) {
|
|
468
|
+
console.error("error:", error.message);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
sources.forget(built.id);
|
|
473
|
+
if (built.digest === deployed) {
|
|
474
|
+
console.error(`built in ${elapsed(started)}; it came out as deployed, so nothing was deployed`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
let result;
|
|
478
|
+
try {
|
|
479
|
+
result = await deploy(client, built.archive, inner);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (unauthenticated(error)) {
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
if (!inner.aborted) {
|
|
485
|
+
printError(error, "deploy");
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
deployed = built.digest;
|
|
490
|
+
printDeployed(result, false);
|
|
491
|
+
console.error(`built and deployed in ${elapsed(started)}; watching ${root}`);
|
|
492
|
+
};
|
|
493
|
+
const watch = () =>
|
|
494
|
+
new Promise((resolve, reject) => {
|
|
495
|
+
let ignore = new Ignorer(root);
|
|
496
|
+
let timer;
|
|
497
|
+
const watcher = fs.watch(root, { recursive: true }, (_, filename) => {
|
|
498
|
+
if (filename === null) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const name = path.join(root, filename);
|
|
502
|
+
if (path.basename(name) === ".gitignore") {
|
|
503
|
+
ignore = new Ignorer(root);
|
|
504
|
+
}
|
|
505
|
+
let isDirectory = false;
|
|
506
|
+
try {
|
|
507
|
+
isDirectory = fs.statSync(name).isDirectory();
|
|
508
|
+
} catch {
|
|
509
|
+
isDirectory = false;
|
|
510
|
+
}
|
|
511
|
+
const relative = relativePath(root, name);
|
|
512
|
+
if (relative === "" || ignore.ignored(relative, isDirectory)) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
clearTimeout(timer);
|
|
516
|
+
timer = setTimeout(() => cycle().catch(reject), rebuildDelay);
|
|
517
|
+
});
|
|
518
|
+
watcher.on("error", error => printError(error, "watch the project"));
|
|
519
|
+
inner.addEventListener(
|
|
520
|
+
"abort",
|
|
521
|
+
() => {
|
|
522
|
+
clearTimeout(timer);
|
|
523
|
+
watcher.close();
|
|
524
|
+
resolve();
|
|
525
|
+
},
|
|
526
|
+
{ once: true },
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
try {
|
|
530
|
+
await cycle();
|
|
531
|
+
await Promise.race([watch(), fatal]);
|
|
532
|
+
} finally {
|
|
533
|
+
controller.abort();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function readScript(args, options) {
|
|
538
|
+
if (args.length === 1 && options.file !== undefined) {
|
|
539
|
+
throw new UsageError("give CODE or --file, not both");
|
|
540
|
+
}
|
|
541
|
+
if (options.file !== undefined) {
|
|
542
|
+
return { code: fs.readFileSync(options.file, "utf8"), run: true };
|
|
543
|
+
}
|
|
544
|
+
if (args.length === 1 && args[0] !== "-") {
|
|
545
|
+
return { code: args[0], run: true };
|
|
546
|
+
}
|
|
547
|
+
if (args.length === 0 && options.reset) {
|
|
548
|
+
return { code: "", run: false };
|
|
549
|
+
}
|
|
550
|
+
return { code: readStdin().toString(), run: true };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
class ImageSink {
|
|
554
|
+
directory = "";
|
|
555
|
+
|
|
556
|
+
constructor(output) {
|
|
557
|
+
this.output = output;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
save(data) {
|
|
561
|
+
if (this.directory === "") {
|
|
562
|
+
if (this.output !== undefined && this.output !== "") {
|
|
563
|
+
this.directory = path.resolve(this.output);
|
|
564
|
+
fs.mkdirSync(this.directory, { recursive: true });
|
|
565
|
+
} else {
|
|
566
|
+
this.directory = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-js-"));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const name = path.join(this.directory, `image-${crypto.randomBytes(5).toString("hex")}.png`);
|
|
570
|
+
fs.writeFileSync(name, data);
|
|
571
|
+
return name;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function js(context) {
|
|
576
|
+
const { args, options, globals } = context;
|
|
577
|
+
if (options.api) {
|
|
578
|
+
if (args.length > 0 || options.file !== undefined || options.reset) {
|
|
579
|
+
throw new UsageError("--api takes no script and no --reset");
|
|
580
|
+
}
|
|
581
|
+
await session(context, async client => {
|
|
582
|
+
process.stdout.write((await client.repl.getDeclarations({}, { signal: context.signal })).typescript);
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const { code, run } = readScript(args, options);
|
|
587
|
+
const signal = globals.timeoutMs > 0 ? AbortSignal.any([context.interrupt, AbortSignal.timeout(globals.timeoutMs + scriptGrace)]) : context.interrupt;
|
|
588
|
+
await session(context, async client => {
|
|
589
|
+
if (options.reset) {
|
|
590
|
+
await client.repl.reset({}, { signal });
|
|
591
|
+
}
|
|
592
|
+
if (!run) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const script = options.file ?? "input.js";
|
|
596
|
+
const images = new ImageSink(options.output);
|
|
597
|
+
let failure;
|
|
598
|
+
for await (const response of client.repl.execute({ code, timeoutMs: globals.timeoutMs }, { signal })) {
|
|
599
|
+
switch (response.part.case) {
|
|
600
|
+
case "console": {
|
|
601
|
+
const output = response.part.value;
|
|
602
|
+
(output.level === LogLevel.ERROR || output.level === LogLevel.WARN ? console.error : console.log)(output.text);
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case "value":
|
|
606
|
+
console.log(response.part.value.text);
|
|
607
|
+
break;
|
|
608
|
+
case "image":
|
|
609
|
+
console.log(`[image: ${images.save(response.part.value)}]`);
|
|
610
|
+
break;
|
|
611
|
+
case "error":
|
|
612
|
+
failure = response.part.value;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (failure) {
|
|
617
|
+
throw printScriptError(failure, frame => (frame.file === "input.js" ? { ...frame, file: script } : frame));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|