@swipt/cli 0.1.0-nightly.39 → 0.1.0-nightly.41

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/connection.mjs CHANGED
@@ -7,7 +7,8 @@ import { setTimeout as sleep } from "node:timers/promises";
7
7
  import Bonjour from "bonjour-service";
8
8
 
9
9
  import { Code, RpcError, UsageError, open } from "./client.mjs";
10
- import { columns } from "./output.mjs";
10
+ import { columns, enumText } from "./output.mjs";
11
+ import { DeviceConnectionProblem, DeviceConnectionProblemSchema, DeviceConnectionStateSchema } from "./proto/common_pb.js";
11
12
 
12
13
  const browseLimit = 1500;
13
14
  const browseRequery = 300;
@@ -128,7 +129,7 @@ async function discover(signal, stored, until) {
128
129
  return found;
129
130
  }
130
131
 
131
- const requiresToken = found => `${found.name} requires a token: swipt connect ${withToken(found.url)}`;
132
+ const requiresToken = found => `${found.name} requires a token: swipt device add ${withToken(found.url)}`;
132
133
 
133
134
  export async function attach(context) {
134
135
  const given = context.options.device ?? process.env.SWIPT_DEVICE;
@@ -142,7 +143,7 @@ export async function attach(context) {
142
143
  if (known === undefined) {
143
144
  const found = (await discover(context.signal, stored, services => services.some(it => sameName(it.name, name)))).find(it => sameName(it.name, name));
144
145
  if (!found) {
145
- throw new Error(`there is no device ${name}; "swipt devices" lists them`);
146
+ throw new Error(`there is no device ${name}; "swipt device list" lists them`);
146
147
  }
147
148
  if (found.authenticates) {
148
149
  throw new Error(requiresToken(found));
@@ -161,7 +162,7 @@ export async function attach(context) {
161
162
  if (locked.length > 0) {
162
163
  throw new Error(locked.map(requiresToken).join("\n"));
163
164
  }
164
- throw new Error('there is no device; turn on Swipt › Developer › Local Network, or connect one with "swipt connect URL"');
165
+ throw new Error('there is no device; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"');
165
166
  }
166
167
  if (names.length > 1) {
167
168
  throw new Error(`there is more than one device: ${names.join(", ")}; choose one with -d or SWIPT_DEVICE`);
@@ -172,35 +173,42 @@ export async function attach(context) {
172
173
  return open(device);
173
174
  }
174
175
 
175
- async function reachable(device, signal) {
176
+ function connectionText(connection) {
177
+ let text = enumText(DeviceConnectionStateSchema, connection?.state ?? 0, "DEVICE_CONNECTION_STATE_");
178
+ if (connection && connection.problem !== DeviceConnectionProblem.UNSPECIFIED) {
179
+ text += `, ${enumText(DeviceConnectionProblemSchema, connection.problem, "DEVICE_CONNECTION_PROBLEM_")}`;
180
+ }
181
+ if (connection?.message) {
182
+ text += `: ${connection.message}`;
183
+ }
184
+ return text;
185
+ }
186
+
187
+ async function probe(url, signal) {
176
188
  try {
177
- await open(device).system.getInfo({}, { signal });
178
- return true;
189
+ const info = await open(parseURL(url)).system.getInfo({}, { signal });
190
+ return [`${info.appVersion} (${info.appBuild})`, connectionText(info.deviceConnection)];
179
191
  } catch (error) {
180
- return error instanceof RpcError && error.code === Code.Unauthenticated;
192
+ return [error instanceof RpcError && error.code === Code.Unauthenticated ? "requires a token" : "offline"];
181
193
  }
182
194
  }
183
195
 
184
- export async function devices({ signal }) {
196
+ export async function list({ signal }) {
185
197
  const stored = load();
186
198
  const deadline = AbortSignal.any([signal, AbortSignal.timeout(browseLimit)]);
187
- const probes = new Map(Object.entries(stored).map(([name, url]) => [name, reachable(parseURL(url), deadline)]));
188
- const found = await discover(signal, stored);
199
+ const probes = new Map(Object.values(stored).map(url => [url, probe(url, deadline)]));
200
+ await discover(signal, stored);
189
201
  const names = Object.keys(stored).sort((a, b) => a.localeCompare(b));
190
202
  if (names.length === 0) {
191
- console.error('no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network, or connect one with "swipt connect URL"');
203
+ console.error('no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"');
192
204
  return;
193
205
  }
194
- const rows = [];
195
- for (const name of names) {
196
- const { url } = parseURL(stored[name]);
197
- const online = found.some(it => sameName(it.name, name) && it.url === url) || (probes.has(name) && (await probes.get(name)));
198
- rows.push([name, url, online ? "" : "offline"]);
199
- }
206
+ const late = AbortSignal.any([signal, AbortSignal.timeout(browseLimit)]);
207
+ const rows = await Promise.all(names.map(async name => [name, parseURL(stored[name]).url, ...(await (probes.get(stored[name]) ?? probe(stored[name], late)))]));
200
208
  console.log(columns(rows));
201
209
  }
202
210
 
203
- export async function connect(context) {
211
+ export async function add(context) {
204
212
  const device = parseURL(context.args[0]);
205
213
  context.target.url = device.url;
206
214
  const info = await open(device).system.getInfo({}, { signal: context.signal });
@@ -212,10 +220,10 @@ export async function connect(context) {
212
220
  }
213
221
  stored[name] = context.args[0];
214
222
  save(stored);
215
- console.log("connected", name);
223
+ console.log("added", name);
216
224
  }
217
225
 
218
- export function disconnect({ args }) {
226
+ export function remove({ args }) {
219
227
  const stored = load();
220
228
  const name = storedName(stored, label(args[0]));
221
229
  if (name === undefined) {
@@ -223,5 +231,5 @@ export function disconnect({ args }) {
223
231
  }
224
232
  delete stored[name];
225
233
  save(stored);
226
- console.log("disconnected", name);
234
+ console.log("removed", name);
227
235
  }
package/device.mjs CHANGED
@@ -13,9 +13,8 @@ import { attach } from "./connection.mjs";
13
13
  import { Ignorer, relativePath } from "./ignore.mjs";
14
14
  import { columns, dateTime, enumText, printLog, printScriptError, serviceText } from "./output.mjs";
15
15
  import { buildProject, loadProject } from "./project.mjs";
16
- import { DeviceConnectionProblem, DeviceConnectionProblemSchema, DeviceConnectionStateSchema, LogLevel } from "./proto/common_pb.js";
16
+ import { LogLevel } from "./proto/common_pb.js";
17
17
  import { ServiceStatus, ServiceStatusSchema } from "./proto/swiplets_pb.js";
18
- import { sources } from "./sourcemap.mjs";
19
18
 
20
19
  const uploadChunk = 256 << 10;
21
20
  const rebuildDelay = 300;
@@ -45,27 +44,6 @@ export function printVersion() {
45
44
  console.log("swipt", version, `node ${process.versions.node}`, `${process.platform}/${process.arch}`);
46
45
  }
47
46
 
48
- export const info = context =>
49
- session(context, async client => {
50
- const found = await client.system.getInfo({}, { signal: context.signal });
51
- const connection = found.deviceConnection;
52
- let state = enumText(DeviceConnectionStateSchema, connection?.state ?? 0, "DEVICE_CONNECTION_STATE_");
53
- if (connection && connection.problem !== DeviceConnectionProblem.UNSPECIFIED) {
54
- state += `, ${enumText(DeviceConnectionProblemSchema, connection.problem, "DEVICE_CONNECTION_PROBLEM_")}`;
55
- }
56
- if (connection?.message) {
57
- state += `: ${connection.message}`;
58
- }
59
- console.log(
60
- columns([
61
- ["device:", found.deviceName],
62
- ["udid:", found.udid],
63
- ["server:", found.serverVersion],
64
- ["connection:", state],
65
- ]),
66
- );
67
- });
68
-
69
47
  export const logs = context =>
70
48
  session(context, async client => {
71
49
  const { options } = context;
@@ -111,25 +89,27 @@ const serviceStatus = state => (state.status === ServiceStatus.UNSPECIFIED ? "st
111
89
 
112
90
  const notInstalled = id => new Error(`there is no swiplet ${JSON.stringify(id)}`);
113
91
 
114
- export const swiplets = context =>
92
+ export const list = context =>
115
93
  session(context, async client => {
116
94
  const { swiplets } = await client.swiplets.listSwiplets({}, { signal: context.signal });
117
- if (context.args.length === 0) {
118
- const rows = swiplets.map(swiplet => {
119
- let service = "";
120
- let status = "";
121
- if (swiplet.service) {
122
- service = swiplet.service.enabled ? "enabled" : "disabled";
123
- status = serviceStatus(swiplet.service);
124
- if (swiplet.service.error) {
125
- status += `: ${swiplet.service.error.name}: ${swiplet.service.error.message}`;
126
- }
95
+ const rows = swiplets.map(swiplet => {
96
+ let service = "";
97
+ let status = "";
98
+ if (swiplet.service) {
99
+ service = swiplet.service.enabled ? "enabled" : "disabled";
100
+ status = serviceStatus(swiplet.service);
101
+ if (swiplet.service.error) {
102
+ status += `: ${swiplet.service.error.name}: ${swiplet.service.error.message}`;
127
103
  }
128
- return [swiplet.id, swiplet.title, swiplet.version, swiplet.actions.map(action => action.name).join(","), service, status];
129
- });
130
- console.log(columns([["ID", "TITLE", "VERSION", "ACTIONS", "SERVICE", "STATUS"], ...rows]));
131
- return;
132
- }
104
+ }
105
+ return [swiplet.id, swiplet.title, swiplet.version, swiplet.actions.map(action => action.name).join(","), service, status];
106
+ });
107
+ console.log(columns([["ID", "TITLE", "VERSION", "ACTIONS", "SERVICE", "STATUS"], ...rows]));
108
+ });
109
+
110
+ export const show = context =>
111
+ session(context, async client => {
112
+ const { swiplets } = await client.swiplets.listSwiplets({}, { signal: context.signal });
133
113
  const swiplet = swiplets.find(it => it.id === context.args[0]);
134
114
  if (!swiplet) {
135
115
  throw notInstalled(context.args[0]);
@@ -139,7 +119,7 @@ export const swiplets = context =>
139
119
  console.log("ui: yes");
140
120
  }
141
121
  if (swiplet.service) {
142
- console.log(`service: ${serviceText(swiplet.id, swiplet.service, sources.frame.bind(sources))}`);
122
+ console.log(`service: ${serviceText(swiplet.id, swiplet.service)}`);
143
123
  }
144
124
  for (const action of swiplet.actions) {
145
125
  console.log();
@@ -173,14 +153,14 @@ export const call = context =>
173
153
  return;
174
154
  }
175
155
  if (result?.outcome.case === "error") {
176
- throw printScriptError(result.outcome.value, sources.frame(id));
156
+ throw printScriptError(result.outcome.value);
177
157
  }
178
158
  throw new Error("the server sent no result");
179
159
  });
180
160
 
181
161
  async function watchServices(client, id, signal) {
182
162
  for await (const response of client.swiplets.watchServices({ swipletId: id }, { signal })) {
183
- console.log(`${response.swipletId}: ${serviceText(response.swipletId, response.service, sources.frame.bind(sources))}`);
163
+ console.log(`${response.swipletId}: ${serviceText(response.swipletId, response.service)}`);
184
164
  }
185
165
  }
186
166
 
@@ -192,7 +172,7 @@ export const setService = enabled => context =>
192
172
  await watchServices(client, id, context.signal);
193
173
  return;
194
174
  }
195
- console.log(`${id}: ${serviceText(id, response.service, sources.frame.bind(sources))}`);
175
+ console.log(`${id}: ${serviceText(id, response.service)}`);
196
176
  });
197
177
 
198
178
  export const serviceWatch = context => session(context, client => watchServices(client, context.args[0] ?? "", context.signal));
@@ -203,16 +183,6 @@ export const remove = context =>
203
183
  console.log("removed", context.args[0]);
204
184
  });
205
185
 
206
- export const filesList = context =>
207
- session(context, async client => {
208
- const [id, directory = ""] = context.args;
209
- const { files } = await client.swiplets.listDataFiles({ swipletId: id, path: directory, recursive: context.options.recursive }, { signal: context.signal });
210
- const rows = files.map(file => [file.directory ? "-" : String(file.size), dateTime(file.modified), file.directory ? `${file.path}/` : file.path]);
211
- if (rows.length > 0) {
212
- console.log(columns(rows));
213
- }
214
- });
215
-
216
186
  const isLocal = name => {
217
187
  const normalized = path.posix.normalize(name);
218
188
  return !path.posix.isAbsolute(normalized) && normalized !== ".." && !normalized.startsWith("../");
@@ -265,97 +235,131 @@ class FileSink {
265
235
 
266
236
  const printSaved = (target, count) => console.error(`saved ${count} file(s) to ${target}`);
267
237
 
268
- export const filesRead = context =>
269
- session(context, async client => {
270
- const [id, given] = context.args;
271
- const output = context.options.output ?? "";
272
- const requested = path.posix.normalize(`/${given}`).replace(/^\/+|\/+$/g, "");
273
- const printing = output === "" || output === "-";
274
- const directory = () =>
275
- new UsageError(
276
- requested === ""
277
- ? `${JSON.stringify(given)} names the data directory of ${id}; save it with --output DIRECTORY`
278
- : `${JSON.stringify(requested)} is a directory in the data directory of ${id}; save it with --output DIRECTORY`,
279
- );
280
- const relativeTo = name => name.slice(requested.length).replace(/^\//, "");
281
- let sink;
282
- let single;
283
- const contents = [];
284
- try {
285
- for await (const response of client.swiplets.readDataFile({ swipletId: id, path: given }, { signal: context.signal })) {
286
- if (response.part.case === "file") {
287
- const file = response.part.value;
288
- if (file.path === requested && !file.directory) {
289
- single = file;
290
- continue;
291
- }
292
- if (!sink) {
293
- if (printing) {
294
- throw directory();
238
+ const dataCommands = target => ({
239
+ list: context =>
240
+ session(context, async client => {
241
+ const {
242
+ id,
243
+ args: [directory = ""],
244
+ } = target(context.args);
245
+ const { files } = await client.swiplets.listDataFiles({ swipletId: id, path: directory, recursive: context.options.recursive }, { signal: context.signal });
246
+ const rows = files.map(file => [file.directory ? "-" : String(file.size), dateTime(file.modified), file.directory ? `${file.path}/` : file.path]);
247
+ if (rows.length > 0) {
248
+ console.log(columns(rows));
249
+ }
250
+ }),
251
+ read: context =>
252
+ session(context, async client => {
253
+ const {
254
+ id,
255
+ where,
256
+ args: [given],
257
+ } = target(context.args);
258
+ const output = context.options.output ?? "";
259
+ const requested = path.posix.normalize(`/${given}`).replace(/^\/+|\/+$/g, "");
260
+ const printing = output === "" || output === "-";
261
+ const directory = () =>
262
+ new UsageError(
263
+ requested === ""
264
+ ? `${JSON.stringify(given)} names ${where}; save it with --output DIRECTORY`
265
+ : `${JSON.stringify(requested)} is a directory in ${where}; save it with --output DIRECTORY`,
266
+ );
267
+ const relativeTo = name => name.slice(requested.length).replace(/^\//, "");
268
+ let sink;
269
+ let single;
270
+ const contents = [];
271
+ try {
272
+ for await (const response of client.swiplets.readDataFile({ swipletId: id, path: given }, { signal: context.signal })) {
273
+ if (response.part.case === "file") {
274
+ const file = response.part.value;
275
+ if (file.path === requested && !file.directory) {
276
+ single = file;
277
+ continue;
295
278
  }
296
- sink = new FileSink(output);
297
- }
298
- sink.begin(relativeTo(file.path), file.directory);
299
- } else if (response.part.case === "chunk") {
300
- const chunk = response.part.value;
301
- if (single && chunk.path === single.path) {
302
- contents.push(chunk.data);
303
- continue;
304
- }
305
- if (!sink) {
306
- throw new Error(`the server sent data for ${chunk.path} before announcing it`);
279
+ if (!sink) {
280
+ if (printing) {
281
+ throw directory();
282
+ }
283
+ sink = new FileSink(output);
284
+ }
285
+ sink.begin(relativeTo(file.path), file.directory);
286
+ } else if (response.part.case === "chunk") {
287
+ const chunk = response.part.value;
288
+ if (single && chunk.path === single.path) {
289
+ contents.push(chunk.data);
290
+ continue;
291
+ }
292
+ if (!sink) {
293
+ throw new Error(`the server sent data for ${chunk.path} before announcing it`);
294
+ }
295
+ sink.write(relativeTo(chunk.path), chunk.offset, chunk.data);
307
296
  }
308
- sink.write(relativeTo(chunk.path), chunk.offset, chunk.data);
309
297
  }
298
+ } finally {
299
+ sink?.close();
310
300
  }
311
- } finally {
312
- sink?.close();
313
- }
314
- if (sink) {
315
- printSaved(sink.root, sink.saved);
316
- return;
317
- }
318
- if (!single) {
301
+ if (sink) {
302
+ printSaved(sink.root, sink.saved);
303
+ return;
304
+ }
305
+ if (!single) {
306
+ if (printing) {
307
+ throw directory();
308
+ }
309
+ fs.mkdirSync(output, { recursive: true });
310
+ printSaved(output, 0);
311
+ return;
312
+ }
313
+ const data = Buffer.concat(contents);
319
314
  if (printing) {
320
- throw directory();
315
+ process.stdout.write(data);
316
+ return;
321
317
  }
322
- fs.mkdirSync(output, { recursive: true });
323
- printSaved(output, 0);
324
- return;
325
- }
326
- const data = Buffer.concat(contents);
327
- if (printing) {
328
- process.stdout.write(data);
329
- return;
330
- }
331
- let target = output;
332
- if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
333
- target = path.join(target, path.posix.basename(single.path));
334
- }
335
- fs.writeFileSync(target, data);
336
- printSaved(target, 1);
337
- });
318
+ let destination = output;
319
+ if (fs.existsSync(destination) && fs.statSync(destination).isDirectory()) {
320
+ destination = path.join(destination, path.posix.basename(single.path));
321
+ }
322
+ fs.writeFileSync(destination, data);
323
+ printSaved(destination, 1);
324
+ }),
325
+ write: context =>
326
+ session(context, async client => {
327
+ const {
328
+ id,
329
+ args: [name, file],
330
+ } = target(context.args);
331
+ const data = file === undefined || file === "-" ? readStdin() : fs.readFileSync(file);
332
+ await client.swiplets.writeDataFile(
333
+ (async function* () {
334
+ yield { part: { case: "header", value: { swipletId: id, path: name } } };
335
+ for (let offset = 0; offset < data.length; offset += uploadChunk) {
336
+ yield { part: { case: "data", value: data.subarray(offset, offset + uploadChunk) } };
337
+ }
338
+ })(),
339
+ { signal: context.signal },
340
+ );
341
+ }),
342
+ remove: context =>
343
+ session(context, async client => {
344
+ const {
345
+ id,
346
+ args: [name],
347
+ } = target(context.args);
348
+ await client.swiplets.removeDataFile({ swipletId: id, path: name, recursive: context.options.recursive }, { signal: context.signal });
349
+ }),
350
+ clear: context =>
351
+ session(context, async client => {
352
+ const { id } = target(context.args);
353
+ const { files } = await client.swiplets.listDataFiles({ swipletId: id }, { signal: context.signal });
354
+ for (const file of files) {
355
+ await client.swiplets.removeDataFile({ swipletId: id, path: file.path, recursive: true }, { signal: context.signal });
356
+ }
357
+ }),
358
+ });
338
359
 
339
- export const filesWrite = context =>
340
- session(context, async client => {
341
- const [id, name, file] = context.args;
342
- const data = file === undefined || file === "-" ? readStdin() : fs.readFileSync(file);
343
- await client.swiplets.writeDataFile(
344
- (async function* () {
345
- yield { part: { case: "header", value: { swipletId: id, path: name } } };
346
- for (let offset = 0; offset < data.length; offset += uploadChunk) {
347
- yield { part: { case: "data", value: data.subarray(offset, offset + uploadChunk) } };
348
- }
349
- })(),
350
- { signal: context.signal },
351
- );
352
- });
360
+ export const swipletData = dataCommands(([id, ...args]) => ({ id, where: `the data directory of ${id}`, args }));
353
361
 
354
- export const filesRemove = context =>
355
- session(context, async client => {
356
- const [id, name] = context.args;
357
- await client.swiplets.removeDataFile({ swipletId: id, path: name, recursive: context.options.recursive }, { signal: context.signal });
358
- });
362
+ export const replData = dataCommands(args => ({ id: "@repl", where: "the data directory of the REPL", args }));
359
363
 
360
364
  async function deploy(client, archive, signal) {
361
365
  const data = fs.readFileSync(archive);
@@ -371,20 +375,19 @@ async function deploy(client, archive, signal) {
371
375
 
372
376
  const printDeployed = (deployed, toStdout) => (toStdout ? console.log : console.error)(deployed.replaced ? "replaced" : "installed", deployed.swiplet.id, deployed.swiplet.version);
373
377
 
374
- export async function deployCommand(context) {
375
- const [given] = context.args;
376
- let archive;
377
- if (given !== undefined && (given.endsWith(".swiplet") || given.endsWith(".zip"))) {
378
- if (!fs.existsSync(given)) {
379
- throw new Error(`there is no archive at ${given}`);
380
- }
381
- archive = given;
382
- } else {
383
- ({ archive } = await buildProject(path.resolve(given ?? ".")));
378
+ export async function install(context) {
379
+ const [archive] = context.args;
380
+ if (!fs.existsSync(archive)) {
381
+ throw new Error(`there is no archive at ${archive}`);
384
382
  }
385
383
  await session(context, async client => printDeployed(await deploy(client, archive, context.signal), true));
386
384
  }
387
385
 
386
+ export async function deployProject(context) {
387
+ const { archive } = await buildProject(path.resolve(context.args[0] ?? "."));
388
+ await session(context, async client => printDeployed(await deploy(client, archive, context.signal), true));
389
+ }
390
+
388
391
  async function follow(signal, fatal, run) {
389
392
  while (!signal.aborted) {
390
393
  try {
@@ -407,7 +410,6 @@ export async function develop(context) {
407
410
  const { signal, printError } = context;
408
411
  const project = loadProject(path.resolve(context.args[0] ?? "."));
409
412
  const root = project.root;
410
- sources.root = root;
411
413
  const client = await attach(context);
412
414
  const controller = new AbortController();
413
415
  const inner = AbortSignal.any([signal, controller.signal]);
@@ -418,7 +420,7 @@ export async function develop(context) {
418
420
  follow(inner, fatalReject, async () => {
419
421
  for await (const response of client.swiplets.watchServices({}, { signal: inner })) {
420
422
  if (response.swipletId === project.config.id && response.service?.status === ServiceStatus.FAILED) {
421
- console.error(`${response.swipletId}: ${serviceText(response.swipletId, response.service, sources.frame.bind(sources))}`);
423
+ console.error(`${response.swipletId}: ${serviceText(response.swipletId, response.service)}`);
422
424
  }
423
425
  }
424
426
  });
@@ -441,7 +443,6 @@ export async function develop(context) {
441
443
  }
442
444
  return;
443
445
  }
444
- sources.forget(built.id);
445
446
  if (built.digest === deployed) {
446
447
  console.error(`built in ${elapsed(started)}; it came out as deployed, so nothing was deployed`);
447
448
  return;
@@ -506,20 +507,27 @@ export async function develop(context) {
506
507
  }
507
508
  }
508
509
 
509
- function readScript(args, options) {
510
- if (args.length === 1 && options.file !== undefined) {
511
- throw new UsageError("give CODE or --file, not both");
510
+ const isFile = name => {
511
+ try {
512
+ return fs.statSync(name).isFile();
513
+ } catch {
514
+ return false;
512
515
  }
513
- if (options.file !== undefined) {
514
- return { code: fs.readFileSync(options.file, "utf8"), run: true };
516
+ };
517
+
518
+ export const isProgram = first => first === "-" || isFile(first);
519
+
520
+ function readScript([first, ...args]) {
521
+ if (first === undefined || first === "-") {
522
+ return { code: readStdin().toString(), args, program: true, file: "input.js" };
515
523
  }
516
- if (args.length === 1 && args[0] !== "-") {
517
- return { code: args[0], run: true };
524
+ if (isFile(first)) {
525
+ return { code: fs.readFileSync(first, "utf8"), args, program: true, file: first };
518
526
  }
519
- if (args.length === 0 && options.reset) {
520
- return { code: "", run: false };
527
+ if (args.length > 0) {
528
+ throw new UsageError(`${JSON.stringify(first)} is not a file, and CODE takes no ARG`);
521
529
  }
522
- return { code: readStdin().toString(), run: true };
530
+ return { code: first, args, program: false, file: "input.js" };
523
531
  }
524
532
 
525
533
  class ImageSink {
@@ -535,7 +543,7 @@ class ImageSink {
535
543
  this.directory = path.resolve(this.output);
536
544
  fs.mkdirSync(this.directory, { recursive: true });
537
545
  } else {
538
- this.directory = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-js-"));
546
+ this.directory = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-repl-"));
539
547
  }
540
548
  }
541
549
  const name = path.join(this.directory, `image-${crypto.randomBytes(5).toString("hex")}.png`);
@@ -544,29 +552,20 @@ class ImageSink {
544
552
  }
545
553
  }
546
554
 
547
- export async function js(context) {
555
+ export const api = context =>
556
+ session(context, async client => {
557
+ process.stdout.write((await client.repl.getDeclarations({}, { signal: context.signal })).typescript);
558
+ });
559
+
560
+ export const reset = context => session(context, client => client.repl.reset({}, { signal: context.signal }));
561
+
562
+ export async function run(context) {
548
563
  const { args, options, signal } = context;
549
- if (options.api) {
550
- if (args.length > 0 || options.file !== undefined || options.reset) {
551
- throw new UsageError("--api takes no script and no --reset");
552
- }
553
- await session(context, async client => {
554
- process.stdout.write((await client.repl.getDeclarations({}, { signal: context.signal })).typescript);
555
- });
556
- return;
557
- }
558
- const { code, run } = readScript(args, options);
564
+ const script = readScript(args);
559
565
  await session(context, async client => {
560
- if (options.reset) {
561
- await client.repl.reset({}, { signal });
562
- }
563
- if (!run) {
564
- return;
565
- }
566
- const script = options.file ?? "input.js";
567
566
  const images = new ImageSink(options.output);
568
567
  let failure;
569
- for await (const response of client.repl.execute({ code }, { signal })) {
568
+ for await (const response of client.repl.execute({ code: script.code, args: script.args, program: script.program }, { signal })) {
570
569
  switch (response.part.case) {
571
570
  case "console": {
572
571
  const output = response.part.value;
@@ -585,7 +584,7 @@ export async function js(context) {
585
584
  }
586
585
  }
587
586
  if (failure) {
588
- throw printScriptError(failure, frame => (frame.file === "input.js" ? { ...frame, file: script } : frame));
587
+ throw printScriptError({ ...failure, stack: failure.stack.map(frame => (frame.file === "input.js" ? { ...frame, file: script.file } : frame)) });
589
588
  }
590
589
  });
591
590
  }
package/output.mjs CHANGED
@@ -36,10 +36,10 @@ const frameText = frame => {
36
36
  return frame.function === "" ? position : `${frame.function} (${position})`;
37
37
  };
38
38
 
39
- export const failureText = (failure, frames) => [`${failure.name}: ${failure.message}`, ...failure.stack.map(frame => ` at ${frameText(frames(frame))}`)].join("\n");
39
+ export const failureText = failure => [`${failure.name}: ${failure.message}`, ...failure.stack.map(frame => ` at ${frameText(frame)}`)].join("\n");
40
40
 
41
- export function printScriptError(failure, frames) {
42
- console.error(`error: ${failureText(failure, frames)}`);
41
+ export function printScriptError(failure) {
42
+ console.error(`error: ${failureText(failure)}`);
43
43
  return new FalseResult();
44
44
  }
45
45
 
@@ -48,14 +48,14 @@ export function printLog(entry, toStdout) {
48
48
  (toStdout ? console.log : console.error)(clockTime(entry.time), levelName(entry.level).toUpperCase(), message);
49
49
  }
50
50
 
51
- export function serviceText(id, state, frames) {
51
+ export function serviceText(id, state) {
52
52
  let text = state?.enabled ? "enabled" : "disabled";
53
53
  text += `, ${state && state.status !== ServiceStatus.UNSPECIFIED ? enumText(ServiceStatusSchema, state.status, "SERVICE_STATUS_") : "stopped"}`;
54
54
  if (state?.since) {
55
55
  text += ` since ${clockTime(state.since)}`;
56
56
  }
57
57
  if (state?.error) {
58
- text += `: ${failureText(state.error, frames(id))}`;
58
+ text += `: ${failureText(state.error)}`;
59
59
  }
60
60
  return text;
61
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swipt/cli",
3
- "version": "0.1.0-nightly.39",
3
+ "version": "0.1.0-nightly.41",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,6 @@
23
23
  "@babel/runtime": "^7.29.10",
24
24
  "@bufbuild/protobuf": "^2.16.0",
25
25
  "@expo/env": "^2.5.3",
26
- "@jridgewell/trace-mapping": "^0.3.31",
27
26
  "babel-preset-expo": "~58.0.22",
28
27
  "bonjour-service": "^1.4.4",
29
28
  "esbuild": "^0.28.2",
@@ -31,7 +30,7 @@
31
30
  "react-native-worklets": "0.13.0"
32
31
  },
33
32
  "peerDependencies": {
34
- "@swipt/api": "0.1.0-nightly.39"
33
+ "@swipt/api": "0.1.0-nightly.41"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@bufbuild/protoc-gen-es": "^2.16.0"
package/project.mjs CHANGED
@@ -26,6 +26,7 @@ const appModules = [
26
26
  "@swipt/api",
27
27
  "@swipt/api/swiplet",
28
28
  "@swipt/api/file-system",
29
+ "expo-sqlite",
29
30
  ];
30
31
  const extensionModules = [
31
32
  "@swipt/api",
@@ -37,6 +38,7 @@ const extensionModules = [
37
38
  "@swipt/api/screen",
38
39
  "@swipt/api/image",
39
40
  "expo-localization",
41
+ "expo-sqlite",
40
42
  ];
41
43
 
42
44
  const idPattern = /^[a-z0-9][a-z0-9._-]{0,127}$/;
@@ -84,9 +86,10 @@ async function bundle(project) {
84
86
  const out = { app: "", extension: "", extensionMap: "", assets: new Map() };
85
87
  loadEnvironment(project.root);
86
88
  if (project.extension) {
89
+ const outfile = path.join(project.root, "dist", project.config.id, "src", "extension.js");
87
90
  const result = await build({
88
91
  entryPoints: [project.extension],
89
- outfile: path.join(project.root, "dist", project.config.id, "src", "extension.js"),
92
+ outfile,
90
93
  bundle: true,
91
94
  format: "cjs",
92
95
  platform: "neutral",
@@ -100,7 +103,9 @@ async function bundle(project) {
100
103
  plugins: [metro(project.root, { sourcemap: true })],
101
104
  });
102
105
  out.extension = result.outputFiles.find(file => file.path.endsWith(".js")).text;
103
- out.extensionMap = result.outputFiles.find(file => file.path.endsWith(".map")).text;
106
+ const map = JSON.parse(result.outputFiles.find(file => file.path.endsWith(".map")).text);
107
+ const sources = map.sources.map(source => path.relative(project.root, path.resolve(path.dirname(outfile), map.sourceRoot ?? "", source)).split(path.sep).join("/"));
108
+ out.extensionMap = JSON.stringify({ ...map, sourceRoot: undefined, sources });
104
109
  }
105
110
  if (project.app) {
106
111
  const result = await build({
@@ -163,6 +168,14 @@ function describeExtension(source) {
163
168
  "@swipt/api/screen": loading("screen"),
164
169
  "@swipt/api/image": loading("image"),
165
170
  "expo-localization": { getLocales: () => [{ languageTag: "en", languageCode: "en" }], getCalendars: () => [] },
171
+ "expo-sqlite": {
172
+ openDatabaseSync: () => loading("the database"),
173
+ openDatabaseAsync: () => Promise.resolve(loading("the database")),
174
+ deleteDatabaseSync: () => {},
175
+ deleteDatabaseAsync: () => Promise.resolve(),
176
+ addDatabaseChangeListener: () => ({ remove: () => {} }),
177
+ defaultDatabaseDirectory: "",
178
+ },
166
179
  };
167
180
  const ignored = () => 0;
168
181
  const context = vm.createContext({
@@ -273,7 +286,7 @@ export async function buildProject(root) {
273
286
  const archive = path.join(dist, `${manifest.id}.swiplet`);
274
287
  fs.rmSync(archive, { force: true });
275
288
  fs.rmSync(path.join(dist, `${manifest.id}.zip`), { force: true });
276
- execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, ...fs.readdirSync(folder), "-x", "*.map"], { cwd: folder });
289
+ execFileSync("zip", ["-qr", "-n", storedSuffixes, archive, ...fs.readdirSync(folder)], { cwd: folder });
277
290
  return { id: manifest.id, archive, digest: folderDigest(folder) };
278
291
  }
279
292
 
@@ -284,7 +297,7 @@ function folderDigest(folder) {
284
297
  const name = path.join(directory, entry.name);
285
298
  if (entry.isDirectory()) {
286
299
  walk(name);
287
- } else if (!entry.name.endsWith(".map")) {
300
+ } else {
288
301
  hash.update(`${path.relative(folder, name)}\0`);
289
302
  hash.update(fs.readFileSync(name));
290
303
  hash.update("\0");
@@ -414,7 +427,7 @@ const templates = {
414
427
  app: "./src/App.tsx",
415
428
  extension: "./src/extension.ts",
416
429
  },
417
- scripts: { build: "swipt build", check: "tsc" },
430
+ scripts: { build: "swipt project build", check: "tsc" },
418
431
  dependencies: { zod: "^4.6.7" },
419
432
  devDependencies: {
420
433
  "@swipt/api": version,
package/proto/repl_pb.js CHANGED
@@ -10,7 +10,7 @@ import { file_common } from "./common_pb.js";
10
10
  * Describes the file repl.proto.
11
11
  */
12
12
  export const file_repl = /*@__PURE__*/
13
- fileDesc("CgpyZXBsLnByb3RvEgVzd2lwdCIyCg5FeGVjdXRlUmVxdWVzdBIMCgRjb2RlGAEgASgJEhIKCnRpbWVvdXRfbXMYAiABKA0iPQoNQ29uc29sZU91dHB1dBIeCgVsZXZlbBgBIAEoDjIPLnN3aXB0LkxvZ0xldmVsEgwKBHRleHQYAiABKAkiKQoLU2NyaXB0VmFsdWUSDAoEanNvbhgBIAEoCRIMCgR0ZXh0GAIgASgJIp0BCg9FeGVjdXRlUmVzcG9uc2USJwoHY29uc29sZRgBIAEoCzIULnN3aXB0LkNvbnNvbGVPdXRwdXRIABIjCgV2YWx1ZRgCIAEoCzISLnN3aXB0LlNjcmlwdFZhbHVlSAASDwoFaW1hZ2UYAyABKAxIABIjCgVlcnJvchgEIAEoCzISLnN3aXB0LlNjcmlwdEVycm9ySABCBgoEcGFydCItChdHZXREZWNsYXJhdGlvbnNSZXNwb25zZRISCgp0eXBlc2NyaXB0GAEgASgJMs0BCgtSRVBMU2VydmljZRI6CgdFeGVjdXRlEhUuc3dpcHQuRXhlY3V0ZVJlcXVlc3QaFi5zd2lwdC5FeGVjdXRlUmVzcG9uc2UwARI3CgVSZXNldBIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJJCg9HZXREZWNsYXJhdGlvbnMSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaHi5zd2lwdC5HZXREZWNsYXJhdGlvbnNSZXNwb25zZUIyWjBnaXRodWIuY29tL3N3aXB0LWF1dG9tYXRpb24vc3dpcHQvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw", [file_google_protobuf_empty, file_common]);
13
+ fileDesc("CgpyZXBsLnByb3RvEgVzd2lwdCJRCg5FeGVjdXRlUmVxdWVzdBIMCgRjb2RlGAEgASgJEhIKCnRpbWVvdXRfbXMYAiABKA0SDAoEYXJncxgDIAMoCRIPCgdwcm9ncmFtGAQgASgIIj0KDUNvbnNvbGVPdXRwdXQSHgoFbGV2ZWwYASABKA4yDy5zd2lwdC5Mb2dMZXZlbBIMCgR0ZXh0GAIgASgJIikKC1NjcmlwdFZhbHVlEgwKBGpzb24YASABKAkSDAoEdGV4dBgCIAEoCSKdAQoPRXhlY3V0ZVJlc3BvbnNlEicKB2NvbnNvbGUYASABKAsyFC5zd2lwdC5Db25zb2xlT3V0cHV0SAASIwoFdmFsdWUYAiABKAsyEi5zd2lwdC5TY3JpcHRWYWx1ZUgAEg8KBWltYWdlGAMgASgMSAASIwoFZXJyb3IYBCABKAsyEi5zd2lwdC5TY3JpcHRFcnJvckgAQgYKBHBhcnQiLQoXR2V0RGVjbGFyYXRpb25zUmVzcG9uc2USEgoKdHlwZXNjcmlwdBgBIAEoCTLNAQoLUkVQTFNlcnZpY2USOgoHRXhlY3V0ZRIVLnN3aXB0LkV4ZWN1dGVSZXF1ZXN0GhYuc3dpcHQuRXhlY3V0ZVJlc3BvbnNlMAESNwoFUmVzZXQSFi5nb29nbGUucHJvdG9idWYuRW1wdHkaFi5nb29nbGUucHJvdG9idWYuRW1wdHkSSQoPR2V0RGVjbGFyYXRpb25zEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Gh4uc3dpcHQuR2V0RGVjbGFyYXRpb25zUmVzcG9uc2VCMlowZ2l0aHViLmNvbS9zd2lwdC1hdXRvbWF0aW9uL3N3aXB0L2ludGVybmFsL3Byb3RvYgZwcm90bzM", [file_google_protobuf_empty, file_common]);
14
14
 
15
15
  /**
16
16
  * Describes the message swipt.ExecuteRequest.
@@ -10,7 +10,7 @@ import { file_common } from "./common_pb.js";
10
10
  * Describes the file system.proto.
11
11
  */
12
12
  export const file_system = /*@__PURE__*/
13
- fileDesc("CgxzeXN0ZW0ucHJvdG8SBXN3aXB0IoABCg9HZXRJbmZvUmVzcG9uc2USFgoOc2VydmVyX3ZlcnNpb24YASABKAkSEwoLZGV2aWNlX25hbWUYAiABKAkSDAoEdWRpZBgDIAEoCRIyChFkZXZpY2VfY29ubmVjdGlvbhgEIAEoCzIXLnN3aXB0LkRldmljZUNvbm5lY3Rpb24ySgoNU3lzdGVtU2VydmljZRI5CgdHZXRJbmZvEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5GhYuc3dpcHQuR2V0SW5mb1Jlc3BvbnNlQjJaMGdpdGh1Yi5jb20vc3dpcHQtYXV0b21hdGlvbi9zd2lwdC9pbnRlcm5hbC9wcm90b2IGcHJvdG8z", [file_google_protobuf_empty, file_common]);
13
+ fileDesc("CgxzeXN0ZW0ucHJvdG8SBXN3aXB0IqgBCg9HZXRJbmZvUmVzcG9uc2USFgoOc2VydmVyX3ZlcnNpb24YASABKAkSEwoLZGV2aWNlX25hbWUYAiABKAkSDAoEdWRpZBgDIAEoCRIyChFkZXZpY2VfY29ubmVjdGlvbhgEIAEoCzIXLnN3aXB0LkRldmljZUNvbm5lY3Rpb24SEwoLYXBwX3ZlcnNpb24YBSABKAkSEQoJYXBwX2J1aWxkGAYgASgJMkoKDVN5c3RlbVNlcnZpY2USOQoHR2V0SW5mbxIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRoWLnN3aXB0LkdldEluZm9SZXNwb25zZUIyWjBnaXRodWIuY29tL3N3aXB0LWF1dG9tYXRpb24vc3dpcHQvaW50ZXJuYWwvcHJvdG9iBnByb3RvMw", [file_google_protobuf_empty, file_common]);
14
14
 
15
15
  /**
16
16
  * Describes the message swipt.GetInfoResponse.
package/swipt.mjs CHANGED
@@ -7,14 +7,12 @@ import * as connection from "./connection.mjs";
7
7
  import * as device from "./device.mjs";
8
8
  import { FalseResult } from "./output.mjs";
9
9
  import { buildProject, createCommand, sourceCommand } from "./project.mjs";
10
- import { sources } from "./sourcemap.mjs";
11
10
 
12
11
  const globalOptions = {
13
- device: { type: "string", short: "d", value: "NAME|URL", help: "the device to use (SWIPT_DEVICE)" },
14
12
  help: { type: "boolean", short: "h", help: "help" },
15
13
  };
16
14
 
17
- const projectOption = { project: { type: "string", value: "DIRECTORY", help: "swiplet project for source maps" } };
15
+ const deviceOption = { device: { type: "string", short: "d", value: "NAME|URL", help: "the device to use (SWIPT_DEVICE)" } };
18
16
 
19
17
  const durationUnits = { ns: 1e-6, us: 1e-3, "µs": 1e-3, ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
20
18
 
@@ -48,101 +46,143 @@ function parseCount(text, option) {
48
46
  return Number(text);
49
47
  }
50
48
 
49
+ const dataCommands = (operand, files) => {
50
+ const before = operand === "" ? 0 : 1;
51
+ return {
52
+ name: "data",
53
+ short: `Manage the files in the data directory of ${operand === "" ? "the REPL" : "a swiplet"}`,
54
+ commands: [
55
+ { name: "list", use: `list ${operand}[PATH]`, short: "List data files", args: [before, before + 1], options: { recursive: { type: "boolean", help: "include subdirectories" } }, run: files.list },
56
+ { name: "read", use: `read ${operand}PATH`, short: "Read a data file or directory", args: [before + 1, before + 1], options: { output: { type: "string", value: "PATH", help: "destination" } }, run: files.read },
57
+ { name: "write", use: `write ${operand}PATH [FILE | -]`, short: "Write a data file", args: [before + 1, before + 2], run: files.write },
58
+ { name: "remove", use: `remove ${operand}PATH`, short: "Remove a data file", args: [before + 1, before + 1], options: { recursive: { type: "boolean", help: "include directory contents" } }, run: files.remove },
59
+ { name: "clear", use: `clear ${operand}`.trimEnd(), short: "Remove every data file, keeping the data directory", args: [before, before], run: files.clear },
60
+ ],
61
+ };
62
+ };
63
+
51
64
  const commands = [
52
65
  {
53
- name: "create",
54
- use: "create DIRECTORY",
55
- short: "Make a swiplet project in a new or empty directory",
56
- args: [1, 1],
57
- run: ({ args }) => createCommand(args[0]),
58
- },
59
- {
60
- name: "build",
61
- short: "Bundle the project in this directory into dist/<swiplet.id>.swiplet",
62
- args: [0, 0],
63
- run: async () => {
64
- const { archive } = await buildProject(process.cwd());
65
- console.log(path.relative(process.cwd(), archive));
66
- },
67
- },
68
- {
69
- name: "source",
70
- use: "source [SWIPLET...]",
71
- short: 'Gather the built swiplets of this directory\'s workspaces, or the given ones, into dist/ with a swiplets.json described by "source" in package.json',
72
- args: [0, Infinity],
73
- run: ({ args }) => sourceCommand(process.cwd(), args),
74
- },
75
- { name: "devices", short: "List the devices, adding the ones found on the local network", args: [0, 0], run: connection.devices },
76
- { name: "connect", use: "connect URL", short: "Add a device by its URL; needed when it is remote or requires a token", args: [1, 1], run: connection.connect },
77
- { name: "disconnect", use: "disconnect NAME", short: "Forget a device", args: [1, 1], run: connection.disconnect },
78
- { name: "info", short: "Show the device and its connection", args: [0, 0], run: device.info },
79
- { name: "version", short: "Print the version", args: [0, 0], run: device.printVersion },
80
- { name: "swiplets", use: "swiplets [ID]", short: "List the swiplets or show one", args: [0, 1], options: projectOption, run: device.swiplets },
81
- {
82
- name: "call",
83
- use: "call ID ACTION",
84
- short: "Call a swiplet action",
85
- args: [2, 2],
86
- options: { input: { type: "string", value: "JSON", help: "input JSON, @FILE or -" }, ...projectOption },
87
- run: device.call,
88
- },
89
- {
90
- name: "service",
91
- short: "Manage swiplet services",
92
- options: projectOption,
66
+ name: "project",
67
+ short: "Make, build and deploy swiplet projects",
93
68
  commands: [
94
- { name: "enable", use: "enable ID", short: "Turn on a swiplet service", args: [1, 1], options: { watch: { type: "boolean", help: "watch the state" } }, run: device.setService(true) },
95
- { name: "disable", use: "disable ID", short: "Turn off a swiplet service", args: [1, 1], options: { watch: { type: "boolean", help: "watch the state" } }, run: device.setService(false) },
96
- { name: "watch", use: "watch [ID]", short: "Watch swiplet services", args: [0, 1], run: device.serviceWatch },
69
+ {
70
+ name: "create",
71
+ use: "create DIRECTORY",
72
+ short: "Make a swiplet project in a new or empty directory",
73
+ args: [1, 1],
74
+ run: ({ args }) => createCommand(args[0]),
75
+ },
76
+ {
77
+ name: "build",
78
+ use: "build [PROJECT]",
79
+ short: "Bundle the project into dist/<swiplet.id>.swiplet",
80
+ args: [0, 1],
81
+ run: async ({ args }) => {
82
+ const { archive } = await buildProject(path.resolve(args[0] ?? "."));
83
+ console.log(path.relative(process.cwd(), archive));
84
+ },
85
+ },
86
+ {
87
+ name: "source",
88
+ use: "source [SWIPLET...]",
89
+ short: 'Gather the built swiplets of this directory\'s workspaces, or the given ones, into dist/ with a swiplets.json described by "source" in package.json',
90
+ args: [0, Infinity],
91
+ run: ({ args }) => sourceCommand(process.cwd(), args),
92
+ },
93
+ { name: "deploy", use: "deploy [PROJECT]", short: "Build the project and install it on the device", args: [0, 1], options: deviceOption, run: device.deployProject },
94
+ {
95
+ name: "develop",
96
+ use: "develop [PROJECT]",
97
+ short: "Build, install and follow a swiplet project as it changes",
98
+ args: [0, 1],
99
+ options: { ...deviceOption, "no-logs": { type: "boolean", help: "without logs" } },
100
+ run: device.develop,
101
+ },
97
102
  ],
98
103
  },
99
- { name: "remove", use: "remove ID", short: "Remove a swiplet", args: [1, 1], run: device.remove },
100
104
  {
101
- name: "files",
102
- short: "Manage swiplet data files",
105
+ name: "device",
106
+ short: "Manage the devices and read their logs",
103
107
  commands: [
104
- { name: "list", use: "list ID [PATH]", short: "List data files", args: [1, 2], options: { recursive: { type: "boolean", help: "include subdirectories" } }, run: device.filesList },
105
- { name: "read", use: "read ID PATH", short: "Read a data file or directory", args: [2, 2], options: { output: { type: "string", value: "PATH", help: "destination" } }, run: device.filesRead },
106
- { name: "write", use: "write ID PATH [FILE | -]", short: "Write a data file", args: [2, 3], run: device.filesWrite },
107
- { name: "remove", use: "remove ID PATH", short: "Remove a data file", args: [2, 2], options: { recursive: { type: "boolean", help: "include directory contents" } }, run: device.filesRemove },
108
+ { name: "list", short: "List the devices, adding the ones found on the local network", args: [0, 0], run: connection.list },
109
+ { name: "add", use: "add URL", short: "Add a device by its URL; needed when it is remote or requires a token", args: [1, 1], run: connection.add },
110
+ { name: "remove", use: "remove NAME", short: "Forget a device", args: [1, 1], run: connection.remove },
111
+ {
112
+ name: "logs",
113
+ short: "Print the logs",
114
+ args: [0, 0],
115
+ options: {
116
+ ...deviceOption,
117
+ follow: { type: "boolean", help: "follow new entries" },
118
+ tail: { type: "string", value: "COUNT", help: "number of past entries (default 100)" },
119
+ since: { type: "string", value: "DURATION", help: "maximum age" },
120
+ level: { type: "string", value: "LEVEL", help: "minimum level (default info)" },
121
+ swiplet: { type: "string", value: "ID", help: "swiplet ID" },
122
+ },
123
+ parse: options => ({ ...options, tail: parseCount(options.tail, "--tail"), since: parseDuration(options.since, "--since"), level: options.level ?? "info" }),
124
+ run: device.logs,
125
+ },
108
126
  ],
109
127
  },
110
- { name: "deploy", use: "deploy [ARCHIVE | PROJECT]", short: "Build and install a swiplet", args: [0, 1], run: device.deployCommand },
111
- {
112
- name: "develop",
113
- use: "develop [PROJECT]",
114
- short: "Build, install and follow a swiplet project as it changes",
115
- args: [0, 1],
116
- options: { "no-logs": { type: "boolean", help: "without logs" } },
117
- run: device.develop,
118
- },
119
128
  {
120
- name: "logs",
121
- short: "Print the logs",
122
- args: [0, 0],
123
- options: {
124
- follow: { type: "boolean", help: "follow new entries" },
125
- tail: { type: "string", value: "COUNT", help: "number of past entries (default 100)" },
126
- since: { type: "string", value: "DURATION", help: "maximum age" },
127
- level: { type: "string", value: "LEVEL", help: "minimum level (default info)" },
128
- swiplet: { type: "string", value: "ID", help: "swiplet ID" },
129
- },
130
- parse: options => ({ ...options, tail: parseCount(options.tail, "--tail"), since: parseDuration(options.since, "--since"), level: options.level ?? "info" }),
131
- run: device.logs,
129
+ name: "swiplet",
130
+ short: "Manage the swiplets on the device",
131
+ options: deviceOption,
132
+ commands: [
133
+ { name: "list", short: "List the swiplets", args: [0, 0], run: device.list },
134
+ { name: "show", use: "show ID", short: "Show a swiplet", args: [1, 1], run: device.show },
135
+ { name: "install", use: "install ARCHIVE", short: "Install a swiplet archive", args: [1, 1], run: device.install },
136
+ { name: "remove", use: "remove ID", short: "Remove a swiplet", args: [1, 1], run: device.remove },
137
+ {
138
+ name: "call",
139
+ use: "call ID ACTION",
140
+ short: "Call a swiplet action",
141
+ args: [2, 2],
142
+ options: { input: { type: "string", value: "JSON", help: "input JSON, @FILE or -" } },
143
+ run: device.call,
144
+ },
145
+ {
146
+ name: "service",
147
+ short: "Manage swiplet services",
148
+ commands: [
149
+ { name: "enable", use: "enable ID", short: "Turn on a swiplet service", args: [1, 1], options: { watch: { type: "boolean", help: "watch the state" } }, run: device.setService(true) },
150
+ { name: "disable", use: "disable ID", short: "Turn off a swiplet service", args: [1, 1], options: { watch: { type: "boolean", help: "watch the state" } }, run: device.setService(false) },
151
+ { name: "watch", use: "watch [ID]", short: "Watch swiplet services", args: [0, 1], run: device.serviceWatch },
152
+ ],
153
+ },
154
+ dataCommands("ID ", device.swipletData),
155
+ ],
132
156
  },
133
157
  {
134
- name: "js",
135
- use: "js [CODE | -]",
136
- short: "Run JavaScript on the device",
137
- args: [0, 1],
138
- options: {
139
- file: { type: "string", value: "FILE", help: "script file" },
140
- reset: { type: "boolean", help: "reset the session first" },
141
- api: { type: "boolean", help: "TypeScript declarations" },
142
- output: { type: "string", value: "DIRECTORY", help: "image directory" },
143
- },
144
- run: device.js,
158
+ name: "repl",
159
+ short: "Run JavaScript in the REPL session on the device",
160
+ options: deviceOption,
161
+ commands: [
162
+ {
163
+ name: "run",
164
+ use: "run [CODE | FILE [ARG...] | - [ARG...]]",
165
+ short: "Run a JavaScript expression or program on the device",
166
+ description: [
167
+ "The first argument is FILE when it names an existing file and CODE otherwise; options go before FILE or -, and every argument after it is an ARG, verbatim.",
168
+ "CODE is an expression; its completion value, awaited if it is a promise, is printed in inspect format: strings quoted, and values with their own toString, which include accessibility snapshots and screen recordings, as that string.",
169
+ "FILE, or standard input given as - or no argument, is a program; only its console output and thrown error are printed, and the global args holds its ARGs.",
170
+ 'Top-level await is available, and top-level bindings persist across runs until "swipt repl reset".',
171
+ "An image given to show(), or as the completion value of CODE, is saved as a PNG file and printed as its path.",
172
+ 'There is no module system, so import is unavailable; the globals are accessibility, input, device, screen, image, dialog, dataDirectory, show and sleep, declared by "swipt repl api".',
173
+ 'dataDirectory is the directory that "swipt repl data" manages.',
174
+ ],
175
+ args: [0, Infinity],
176
+ passthrough: device.isProgram,
177
+ options: { output: { type: "string", value: "DIRECTORY", help: "image directory (default: a new temporary directory)" } },
178
+ run: device.run,
179
+ },
180
+ { name: "reset", short: "Reset the session", args: [0, 0], run: device.reset },
181
+ { name: "api", short: "Print the TypeScript declarations of the globals", args: [0, 0], run: device.api },
182
+ dataCommands("", device.replData),
183
+ ],
145
184
  },
185
+ { name: "version", short: "Print the version", args: [0, 0], run: device.printVersion },
146
186
  ];
147
187
 
148
188
  const root = { name: "swipt", short: "Operate a device through Swipt", commands };
@@ -167,6 +207,9 @@ function usage(trail) {
167
207
  } else {
168
208
  lines.push(`usage: ${trail.slice(0, -1).map(it => it.name).join(" ")} ${command.use ?? command.name}`.replace(/ +/g, " "));
169
209
  }
210
+ if (command.description) {
211
+ lines.push("", ...command.description);
212
+ }
170
213
  const options = { ...trail.reduce((found, it) => ({ ...found, ...it.options }), {}), ...globalOptions };
171
214
  lines.push("", "options:");
172
215
  lines.push(...columns(Object.entries(options).map(([option, it]) => [` ${it.short ? `-${it.short}, ` : ""}--${option}${it.value ? ` ${it.value}` : ""}`, it.help])).split("\n"));
@@ -179,18 +222,19 @@ function columns(rows) {
179
222
  }
180
223
 
181
224
  function resolve(argv) {
182
- const loose = parseArgs({ args: argv, options: { ...parseOptions(globalOptions), ...allOptions(root) }, allowPositionals: true, strict: false });
225
+ const loose = parseArgs({ args: argv, options: { ...parseOptions(globalOptions), ...allOptions(root) }, allowPositionals: true, strict: false, tokens: true });
226
+ const positionals = loose.tokens.filter(token => token.kind === "positional");
183
227
  const trail = [root];
184
228
  let consumed = 0;
185
- let help = loose.positionals[0] === "help";
229
+ let help = positionals[0]?.value === "help";
186
230
  if (help) {
187
231
  consumed++;
188
232
  }
189
233
  while (trail.at(-1).commands) {
190
234
  const current = trail.at(-1);
191
- const word = loose.positionals[consumed];
235
+ const word = positionals[consumed]?.value;
192
236
  if (word === undefined) {
193
- if (help || current !== root || argv.length === 0) {
237
+ if (help || loose.values.help || current !== root || argv.length === 0) {
194
238
  return { trail, help: true };
195
239
  }
196
240
  throw new UsageError("a command is missing");
@@ -202,18 +246,21 @@ function resolve(argv) {
202
246
  trail.push(found);
203
247
  consumed++;
204
248
  }
249
+ const command = trail.at(-1);
205
250
  const options = trail.reduce((found, it) => ({ ...found, ...it.options }), {});
251
+ const first = positionals[consumed];
252
+ const verbatim = first !== undefined && command.passthrough?.(first.value) ? first.index + 1 : argv.length;
206
253
  let parsed;
207
254
  try {
208
- parsed = parseArgs({ args: argv, options: { ...parseOptions(globalOptions), ...parseOptions(options) }, allowPositionals: true, strict: true });
255
+ parsed = parseArgs({ args: argv.slice(0, verbatim), options: { ...parseOptions(globalOptions), ...parseOptions(options) }, allowPositionals: true, strict: true });
209
256
  } catch (error) {
210
- throw new UsageError(error.message);
257
+ const option = error.message.match(/^Unknown option '(-[^']*)'/)?.[1];
258
+ throw new UsageError(option === undefined ? error.message : `${option} is not an option of ${JSON.stringify(trail.map(it => it.name).join(" "))}`);
211
259
  }
212
260
  if (parsed.values.help) {
213
261
  help = true;
214
262
  }
215
- const args = parsed.positionals.slice(consumed);
216
- const command = trail.at(-1);
263
+ const args = [...parsed.positionals.slice(consumed), ...argv.slice(verbatim)];
217
264
  if (!help && (args.length < command.args[0] || args.length > command.args[1])) {
218
265
  const [least, most] = command.args;
219
266
  const expected = least === most ? `${least} arg(s)` : most === Infinity ? `at least ${least} arg(s)` : `between ${least} and ${most} arg(s)`;
@@ -234,7 +281,7 @@ function printError(error, target, operation) {
234
281
  message = `Swipt at ${target.url} is unreachable: ${message}`;
235
282
  break;
236
283
  case Code.Unauthenticated:
237
- message += `; connect with the URL from Swipt › Developer: swipt connect ${connection.withToken(target.url)}`;
284
+ message += `; connect with the URL from Swipt › Developer: swipt device add ${connection.withToken(target.url)}`;
238
285
  break;
239
286
  case Code.Unimplemented:
240
287
  message = `Swipt at ${target.url} does not have this command; update Swipt`;
@@ -262,9 +309,6 @@ async function main() {
262
309
  }
263
310
  const command = resolved.trail.at(-1);
264
311
  const values = command.parse ? command.parse(resolved.values) : resolved.values;
265
- if (values.project !== undefined) {
266
- sources.root = path.resolve(values.project);
267
- }
268
312
  await command.run({ args: resolved.args, options: values, target, signal: interrupt.signal, printError: (error, operation) => printError(error, target, operation) });
269
313
  } catch (error) {
270
314
  if (error instanceof FalseResult) {
package/sourcemap.mjs DELETED
@@ -1,77 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
5
-
6
- import { loadProject } from "./project.mjs";
7
-
8
- const suffix = ".map";
9
-
10
- function loadSourceMap(name) {
11
- const raw = JSON.parse(fs.readFileSync(name, "utf8"));
12
- const directory = path.dirname(name);
13
- const working = process.cwd();
14
- const resolve = source => {
15
- const resolved = path.join(directory, raw.sourceRoot ?? "", source);
16
- const relative = path.relative(working, resolved);
17
- return relative.startsWith("..") || path.isAbsolute(relative) ? resolved : relative;
18
- };
19
- const map = new TraceMap({ ...raw, sourceRoot: undefined });
20
- return (line, column) => {
21
- const found = originalPositionFor(map, { line, column: column - 1 });
22
- return found.source === null ? undefined : { file: resolve(found.source), line: found.line, column: found.column + 1 };
23
- };
24
- }
25
-
26
- export class SourceMaps {
27
- root = "";
28
- loaded = new Map();
29
-
30
- forSwiplet(id) {
31
- if (this.root === "") {
32
- return {};
33
- }
34
- if (this.loaded.has(id)) {
35
- return this.loaded.get(id);
36
- }
37
- const found = {};
38
- let project;
39
- try {
40
- project = loadProject(this.root);
41
- } catch {
42
- project = undefined;
43
- }
44
- if (project?.config.id === id) {
45
- const directory = path.join(project.root, "dist", id, "src");
46
- let names = [];
47
- try {
48
- names = fs.readdirSync(directory).filter(name => name.endsWith(suffix));
49
- } catch {
50
- names = [];
51
- }
52
- for (const name of names) {
53
- try {
54
- found[name.slice(0, -suffix.length)] = loadSourceMap(path.join(directory, name));
55
- } catch {
56
- continue;
57
- }
58
- }
59
- }
60
- this.loaded.set(id, found);
61
- return found;
62
- }
63
-
64
- forget(id) {
65
- this.loaded.delete(id);
66
- }
67
-
68
- frame(id) {
69
- return frame => {
70
- const lookup = this.forSwiplet(id)[frame.file];
71
- const mapped = lookup?.(frame.line, frame.column);
72
- return mapped ? { ...frame, ...mapped } : frame;
73
- };
74
- }
75
- }
76
-
77
- export const sources = new SourceMaps();