@x-tech-laboratories/x-libs 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/react/menus.js +11 -7
- package/package.json +1 -1
- package/dist/agent/client.d.ts +0 -32
- package/dist/agent/client.js +0 -148
- package/dist/agent/commands.d.ts +0 -23
- package/dist/agent/commands.js +0 -118
- package/dist/agent/keys.d.ts +0 -14
- package/dist/agent/keys.js +0 -81
- package/dist/agent/long_runs.d.ts +0 -32
- package/dist/agent/long_runs.js +0 -316
- package/dist/agent/protocol.d.ts +0 -148
- package/dist/agent/protocol.js +0 -131
- package/dist/agent/session.d.ts +0 -50
- package/dist/agent/session.js +0 -225
- package/dist/agent/tasks.d.ts +0 -17
- package/dist/agent/tasks.js +0 -15
- package/dist/backups.d.ts +0 -31
- package/dist/backups.js +0 -43
- package/dist/dates.d.ts +0 -9
- package/dist/dates.js +0 -60
- package/dist/durations.d.ts +0 -11
- package/dist/durations.js +0 -49
- package/dist/electron/app.d.ts +0 -13
- package/dist/electron/app.js +0 -27
- package/dist/electron/bridge.d.ts +0 -55
- package/dist/electron/bridge.js +0 -105
- package/dist/electron/events.d.ts +0 -21
- package/dist/electron/events.js +0 -25
- package/dist/electron/fetch.d.ts +0 -52
- package/dist/electron/fetch.js +0 -175
- package/dist/electron/window.d.ts +0 -96
- package/dist/electron/window.js +0 -128
- package/dist/log.d.ts +0 -30
- package/dist/log.js +0 -79
- package/dist/server/agents/agent.d.ts +0 -27
- package/dist/server/agents/agent.js +0 -16
- package/dist/server/agents/connection.d.ts +0 -51
- package/dist/server/agents/connection.js +0 -239
- package/dist/server/agents/hub.d.ts +0 -86
- package/dist/server/agents/hub.js +0 -241
- package/dist/server/agents/local.d.ts +0 -24
- package/dist/server/agents/local.js +0 -62
- package/dist/server/agents/route.d.ts +0 -3
- package/dist/server/agents/route.js +0 -24
- package/dist/server/backup_files.d.ts +0 -3
- package/dist/server/backup_files.js +0 -25
- package/dist/server/config.d.ts +0 -39
- package/dist/server/config.js +0 -109
- package/dist/server/cors.d.ts +0 -8
- package/dist/server/cors.js +0 -13
- package/dist/server/events.d.ts +0 -33
- package/dist/server/events.js +0 -68
- package/dist/server/hash_password.d.ts +0 -1
- package/dist/server/hash_password.js +0 -46
- package/dist/server/login_page.d.ts +0 -8
- package/dist/server/login_page.js +0 -79
- package/dist/server/passwords.d.ts +0 -8
- package/dist/server/passwords.js +0 -59
- package/dist/server/replies.d.ts +0 -12
- package/dist/server/replies.js +0 -41
- package/dist/server/sign_in.d.ts +0 -27
- package/dist/server/sign_in.js +0 -95
- package/dist/server/web_folder.d.ts +0 -11
- package/dist/server/web_folder.js +0 -40
- package/dist/web/dom.d.ts +0 -7
- package/dist/web/dom.js +0 -32
- package/dist/web/drag.d.ts +0 -3
- package/dist/web/drag.js +0 -13
- package/dist/web/errors.d.ts +0 -1
- package/dist/web/errors.js +0 -27
- package/dist/web/format.d.ts +0 -11
- package/dist/web/format.js +0 -82
- package/dist/web/menus.d.ts +0 -17
- package/dist/web/menus.js +0 -40
- package/dist/web/prefs.d.ts +0 -12
- package/dist/web/prefs.js +0 -62
- package/dist/web/react/badges.d.ts +0 -35
- package/dist/web/react/badges.js +0 -32
- package/dist/web/react/controls.d.ts +0 -86
- package/dist/web/react/controls.js +0 -154
- package/dist/web/react/dialog.d.ts +0 -45
- package/dist/web/react/dialog.js +0 -82
- package/dist/web/react/following.d.ts +0 -25
- package/dist/web/react/following.js +0 -34
- package/dist/web/react/layout.d.ts +0 -111
- package/dist/web/react/layout.js +0 -85
- package/dist/web/react/list_picker.d.ts +0 -19
- package/dist/web/react/list_picker.js +0 -50
- package/dist/web/react/markdown.d.ts +0 -8
- package/dist/web/react/markdown.js +0 -40
- package/dist/web/react/media.d.ts +0 -24
- package/dist/web/react/media.js +0 -18
- package/dist/web/react/menu_button.d.ts +0 -10
- package/dist/web/react/menu_button.js +0 -16
- package/dist/web/react/menus.d.ts +0 -31
- package/dist/web/react/page_view.d.ts +0 -16
- package/dist/web/react/page_view.js +0 -6
- package/dist/web/react/prefs.d.ts +0 -2
- package/dist/web/react/prefs.js +0 -11
- package/dist/web/react/progress.d.ts +0 -10
- package/dist/web/react/progress.js +0 -9
- package/dist/web/react/rating.d.ts +0 -8
- package/dist/web/react/rating.js +0 -23
- package/dist/web/react/reveal.d.ts +0 -10
- package/dist/web/react/reveal.js +0 -30
- package/dist/web/react/shell.d.ts +0 -46
- package/dist/web/react/shell.js +0 -94
- package/dist/web/react/shortcuts.d.ts +0 -23
- package/dist/web/react/shortcuts.js +0 -95
- package/dist/web/react/split.d.ts +0 -23
- package/dist/web/react/split.js +0 -94
- package/dist/web/react/states.d.ts +0 -36
- package/dist/web/react/states.js +0 -64
- package/dist/web/react/store.d.ts +0 -4
- package/dist/web/react/store.js +0 -7
- package/dist/web/react/suggestions.d.ts +0 -23
- package/dist/web/react/suggestions.js +0 -57
- package/dist/web/react/table.d.ts +0 -78
- package/dist/web/react/table.js +0 -404
- package/dist/web/react/test_stubs.d.ts +0 -1
- package/dist/web/react/test_stubs.js +0 -25
- package/dist/web/react/time.d.ts +0 -5
- package/dist/web/react/time.js +0 -19
- package/dist/web/react/toasts.d.ts +0 -29
- package/dist/web/react/toasts.js +0 -72
- package/dist/web/react/tree.d.ts +0 -28
- package/dist/web/react/tree.js +0 -183
- package/dist/web/react/volume.d.ts +0 -8
- package/dist/web/react/volume.js +0 -42
- package/dist/web/react/watch.d.ts +0 -3
- package/dist/web/react/watch.js +0 -5
- package/dist/web/router.d.ts +0 -7
- package/dist/web/router.js +0 -29
- package/dist/web/selection.d.ts +0 -25
- package/dist/web/selection.js +0 -106
- package/dist/web/split.d.ts +0 -15
- package/dist/web/split.js +0 -104
- package/dist/web/states.d.ts +0 -3
- package/dist/web/states.js +0 -10
- package/dist/web/store.d.ts +0 -11
- package/dist/web/store.js +0 -17
- package/dist/web/tables.d.ts +0 -28
- package/dist/web/tables.js +0 -88
- package/dist/web/theme.d.ts +0 -14
- package/dist/web/theme.js +0 -52
- package/dist/web/tree.d.ts +0 -40
- package/dist/web/tree.js +0 -130
- package/dist/web/virtual.d.ts +0 -7
- package/dist/web/virtual.js +0 -10
- package/dist/web/watch.d.ts +0 -11
- package/dist/web/watch.js +0 -37
package/dist/agent/session.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// One connection to the server: says hello, with its signature of the server's challenge when it logs in with a key,
|
|
2
|
-
// answers each request, and sends streamed output only as far as the server's credit allows. Closing it stops the
|
|
3
|
-
// streams it was sending; long runs carry on.
|
|
4
|
-
import { sign_challenge } from "./keys.js";
|
|
5
|
-
import { encode_chunk, encode_message, is_server_message, parse_server_message, } from "./protocol.js";
|
|
6
|
-
const socket_open = 1;
|
|
7
|
-
// ── Output ───────────────────────────────────────────────────────────────────
|
|
8
|
-
// Sends a stream's stdout as binary frames within the credit granted, then its end once all of it has gone.
|
|
9
|
-
class OutputPump {
|
|
10
|
-
id;
|
|
11
|
-
stream;
|
|
12
|
-
session;
|
|
13
|
-
credit;
|
|
14
|
-
pending = [];
|
|
15
|
-
stdout_ended = false;
|
|
16
|
-
result = null;
|
|
17
|
-
finished = false;
|
|
18
|
-
constructor(id, stream, window, session) {
|
|
19
|
-
this.id = id;
|
|
20
|
-
this.stream = stream;
|
|
21
|
-
this.credit = window;
|
|
22
|
-
this.session = session;
|
|
23
|
-
stream.stdout.on("data", (chunk) => {
|
|
24
|
-
this.pending.push(chunk);
|
|
25
|
-
this.flush();
|
|
26
|
-
});
|
|
27
|
-
stream.stdout.on("error", () => { });
|
|
28
|
-
stream.stdout.on("close", () => {
|
|
29
|
-
this.stdout_ended = true;
|
|
30
|
-
this.finish_when_done();
|
|
31
|
-
});
|
|
32
|
-
void stream.done.then((result) => {
|
|
33
|
-
this.result = result;
|
|
34
|
-
this.finish_when_done();
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
grant(bytes) {
|
|
38
|
-
this.credit += Math.max(0, bytes);
|
|
39
|
-
this.flush();
|
|
40
|
-
}
|
|
41
|
-
// Stops the command and drops what it had yet to send.
|
|
42
|
-
cancel() {
|
|
43
|
-
this.pending.length = 0;
|
|
44
|
-
this.stream.stop();
|
|
45
|
-
this.stream.stdout.resume();
|
|
46
|
-
}
|
|
47
|
-
flush() {
|
|
48
|
-
while (this.credit > 0 && this.pending.length > 0) {
|
|
49
|
-
const chunk = this.pending[0];
|
|
50
|
-
const piece = chunk.length > this.credit ? chunk.subarray(0, this.credit) : chunk;
|
|
51
|
-
if (piece === chunk)
|
|
52
|
-
this.pending.shift();
|
|
53
|
-
else
|
|
54
|
-
this.pending[0] = chunk.subarray(piece.length);
|
|
55
|
-
this.credit -= piece.length;
|
|
56
|
-
this.session.send_chunk(this.id, piece);
|
|
57
|
-
}
|
|
58
|
-
if (this.pending.length > 0)
|
|
59
|
-
this.stream.stdout.pause();
|
|
60
|
-
else if (!this.stdout_ended)
|
|
61
|
-
this.stream.stdout.resume();
|
|
62
|
-
this.finish_when_done();
|
|
63
|
-
}
|
|
64
|
-
finish_when_done() {
|
|
65
|
-
if (this.finished || !this.stdout_ended || this.result === null || this.pending.length > 0)
|
|
66
|
-
return;
|
|
67
|
-
this.finished = true;
|
|
68
|
-
this.session.end_stream(this.id, this.result);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// ── Session ──────────────────────────────────────────────────────────────────
|
|
72
|
-
export class AgentSession {
|
|
73
|
-
socket;
|
|
74
|
-
runs;
|
|
75
|
-
options;
|
|
76
|
-
pumps = new Map();
|
|
77
|
-
last_heard = Date.now();
|
|
78
|
-
heartbeat;
|
|
79
|
-
closed = false;
|
|
80
|
-
greeted = false;
|
|
81
|
-
constructor(socket, runs, options) {
|
|
82
|
-
this.socket = socket;
|
|
83
|
-
this.runs = runs;
|
|
84
|
-
this.options = options;
|
|
85
|
-
this.heartbeat = setInterval(() => this.beat(), options.heartbeat_in_milliseconds);
|
|
86
|
-
this.heartbeat.unref();
|
|
87
|
-
if (!options.key)
|
|
88
|
-
this.say_hello();
|
|
89
|
-
}
|
|
90
|
-
say_hello(signature) {
|
|
91
|
-
this.greeted = true;
|
|
92
|
-
const hello = { type: "hello", protocol: this.options.app.protocol_version, host: this.options.host, agent_version: this.options.agent_version, runs: this.runs.list() };
|
|
93
|
-
this.send(signature === undefined ? hello : { ...hello, signature });
|
|
94
|
-
this.options.on_hello?.(this);
|
|
95
|
-
}
|
|
96
|
-
// Handles one text frame from the server; anything that is not a message is ignored.
|
|
97
|
-
receive(text) {
|
|
98
|
-
this.last_heard = Date.now();
|
|
99
|
-
const message = parse_server_message(text, this.options.app);
|
|
100
|
-
if (message === null)
|
|
101
|
-
return;
|
|
102
|
-
if (is_server_message(message))
|
|
103
|
-
this.handle(message);
|
|
104
|
-
else
|
|
105
|
-
this.handle_app(message);
|
|
106
|
-
}
|
|
107
|
-
// The connection has gone: stops the streams that were being sent, since nobody reads them now.
|
|
108
|
-
close() {
|
|
109
|
-
if (this.closed)
|
|
110
|
-
return;
|
|
111
|
-
this.closed = true;
|
|
112
|
-
clearInterval(this.heartbeat);
|
|
113
|
-
for (const pump of this.pumps.values())
|
|
114
|
-
pump.cancel();
|
|
115
|
-
this.pumps.clear();
|
|
116
|
-
}
|
|
117
|
-
// ── Sending ──────────────────────────────────────────────────────────────
|
|
118
|
-
send(message) {
|
|
119
|
-
if (!this.closed && this.socket.readyState === socket_open)
|
|
120
|
-
this.socket.send(encode_message(message, this.options.app));
|
|
121
|
-
}
|
|
122
|
-
send_chunk(id, data) {
|
|
123
|
-
if (!this.closed && this.socket.readyState === socket_open)
|
|
124
|
-
this.socket.send(encode_chunk(id, data));
|
|
125
|
-
}
|
|
126
|
-
end_stream(id, result) {
|
|
127
|
-
this.pumps.delete(id);
|
|
128
|
-
this.send({ type: "stream_end", id, exit_code: result.exit_code, stderr: result.stderr });
|
|
129
|
-
}
|
|
130
|
-
// ── Handling ─────────────────────────────────────────────────────────────
|
|
131
|
-
handle(message) {
|
|
132
|
-
switch (message.type) {
|
|
133
|
-
case "ping":
|
|
134
|
-
this.send({ type: "pong", epoch_ms: message.epoch_ms });
|
|
135
|
-
return;
|
|
136
|
-
case "pong":
|
|
137
|
-
return;
|
|
138
|
-
case "challenge":
|
|
139
|
-
if (this.options.key && !this.greeted)
|
|
140
|
-
this.say_hello(sign_challenge(this.options.key, this.options.app.name, this.options.host, message.nonce));
|
|
141
|
-
return;
|
|
142
|
-
case "credit":
|
|
143
|
-
this.pumps.get(message.id)?.grant(message.bytes);
|
|
144
|
-
return;
|
|
145
|
-
case "cancel":
|
|
146
|
-
this.pumps.get(message.id)?.cancel();
|
|
147
|
-
return;
|
|
148
|
-
case "stream":
|
|
149
|
-
this.start_pump(message.id, () => this.open_stream(message.task, message.stdin), message.window);
|
|
150
|
-
return;
|
|
151
|
-
case "attach":
|
|
152
|
-
this.start_pump(message.id, () => this.runs.tail(message.run_id, message.offset), message.window);
|
|
153
|
-
return;
|
|
154
|
-
default:
|
|
155
|
-
void this.answer(message.id, () => this.value_for(message));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// A message carrying an id is a request the app answers; anything else goes to the app as it is.
|
|
159
|
-
handle_app(message) {
|
|
160
|
-
if (typeof message.id !== "string") {
|
|
161
|
-
this.options.on_message?.(message, this);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const requests = this.options.requests ?? {};
|
|
165
|
-
const handler = Object.hasOwn(requests, message.type) ? requests[message.type] : undefined;
|
|
166
|
-
if (handler === undefined) {
|
|
167
|
-
this.send({ type: "failed", id: message.id, error: `This agent does not know the request '${message.type}'.` });
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
void this.answer(message.id, () => handler(message));
|
|
171
|
-
}
|
|
172
|
-
start_pump(id, open, window) {
|
|
173
|
-
if (this.pumps.has(id))
|
|
174
|
-
return;
|
|
175
|
-
let stream;
|
|
176
|
-
try {
|
|
177
|
-
stream = open();
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
this.send({ type: "stream_end", id, exit_code: -1, stderr: error.message });
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
this.pumps.set(id, new OutputPump(id, stream, window, this));
|
|
184
|
-
}
|
|
185
|
-
async answer(id, work) {
|
|
186
|
-
try {
|
|
187
|
-
const value = await work();
|
|
188
|
-
this.send({ type: "reply", id, value: value ?? null });
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
this.send({ type: "failed", id, error: error.message });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
open_stream(task, stdin) {
|
|
195
|
-
if (this.options.runner.stream === undefined)
|
|
196
|
-
throw new Error("This agent does not stream its tasks.");
|
|
197
|
-
return this.options.runner.stream(task, { stdin });
|
|
198
|
-
}
|
|
199
|
-
async value_for(message) {
|
|
200
|
-
switch (message.type) {
|
|
201
|
-
case "run":
|
|
202
|
-
return this.options.runner.run(message.task, { timeout_in_seconds: message.timeout_in_seconds, stdin: message.stdin });
|
|
203
|
-
case "start":
|
|
204
|
-
return this.runs.start(message.run_id, message.task, message.stdin);
|
|
205
|
-
case "poll":
|
|
206
|
-
return this.runs.poll(message.run_id, message.offset);
|
|
207
|
-
case "stop":
|
|
208
|
-
return this.runs.stop(message.run_id);
|
|
209
|
-
case "forget":
|
|
210
|
-
this.runs.forget(message.run_id);
|
|
211
|
-
return null;
|
|
212
|
-
case "runs":
|
|
213
|
-
return this.runs.list();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Pings the server, or gives up on one that has been silent for three beats.
|
|
217
|
-
beat() {
|
|
218
|
-
if (Date.now() - this.last_heard > 3 * this.options.heartbeat_in_milliseconds) {
|
|
219
|
-
this.socket.close(4008, "The server stopped answering.");
|
|
220
|
-
this.close();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.send({ type: "ping", epoch_ms: Date.now() });
|
|
224
|
-
}
|
|
225
|
-
}
|
package/dist/agent/tasks.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type CommandStream } from "./commands.ts";
|
|
2
|
-
import type { RunResult } from "./protocol.ts";
|
|
3
|
-
export interface LongRunLaunch {
|
|
4
|
-
program: string;
|
|
5
|
-
args: string[];
|
|
6
|
-
}
|
|
7
|
-
export interface TaskRunner<Task> {
|
|
8
|
-
run(task: Task, options: {
|
|
9
|
-
timeout_in_seconds: number;
|
|
10
|
-
stdin?: string | null;
|
|
11
|
-
}): Promise<RunResult>;
|
|
12
|
-
stream?(task: Task, options: {
|
|
13
|
-
stdin?: string | null;
|
|
14
|
-
}): CommandStream;
|
|
15
|
-
launch(task: Task, folder: string, has_stdin: boolean): LongRunLaunch;
|
|
16
|
-
}
|
|
17
|
-
export declare function shell_runner(process_name: string): TaskRunner<string>;
|
package/dist/agent/tasks.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// How an agent carries out the app's tasks: to their end, as a stream, or as a long run's own process. Shell tasks, a
|
|
2
|
-
// command line run by sh, come built in; an app with typed operations gives its own.
|
|
3
|
-
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
4
|
-
import { run_command, stream_command } from "./commands.js";
|
|
5
|
-
// ── Shell ────────────────────────────────────────────────────────────────────
|
|
6
|
-
// The outer shell runs the command with its output in the run's folder, then writes the exit code in one rename.
|
|
7
|
-
const wrapper = 'sh -c "$2" > "$1/out" 2>&1; echo $? > "$1/exit.part"; mv -f "$1/exit.part" "$1/exit"';
|
|
8
|
-
// Runs each task as a command line under sh; `process_name`, such as "x-backups-run", is what ps shows for a long run.
|
|
9
|
-
export function shell_runner(process_name) {
|
|
10
|
-
return {
|
|
11
|
-
run: (command, options) => run_command(command, options),
|
|
12
|
-
stream: (command, options) => stream_command(command, options),
|
|
13
|
-
launch: (command, folder) => ({ program: "sh", args: ["-c", wrapper, process_name, folder, command] }),
|
|
14
|
-
};
|
|
15
|
-
}
|
package/dist/backups.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export interface Backup {
|
|
2
|
-
name: string;
|
|
3
|
-
taken_at: string;
|
|
4
|
-
is_automatic: boolean;
|
|
5
|
-
size_bytes: number;
|
|
6
|
-
}
|
|
7
|
-
export interface BackupNaming {
|
|
8
|
-
pattern: RegExp;
|
|
9
|
-
name_for: (taken_at: Date, is_automatic: boolean) => string;
|
|
10
|
-
describe: (name: string, size_bytes: number) => Backup;
|
|
11
|
-
}
|
|
12
|
-
export declare function backup_naming(prefix: string, extension: string): BackupNaming;
|
|
13
|
-
export interface BackupStore {
|
|
14
|
-
list: () => Promise<{
|
|
15
|
-
name: string;
|
|
16
|
-
size_bytes: number;
|
|
17
|
-
}[]> | {
|
|
18
|
-
name: string;
|
|
19
|
-
size_bytes: number;
|
|
20
|
-
}[];
|
|
21
|
-
take: (name: string) => Promise<number> | number;
|
|
22
|
-
remove: (name: string) => Promise<void> | void;
|
|
23
|
-
}
|
|
24
|
-
export interface Backups {
|
|
25
|
-
naming: BackupNaming;
|
|
26
|
-
list: () => Promise<Backup[]>;
|
|
27
|
-
create: (is_automatic?: boolean) => Promise<Backup>;
|
|
28
|
-
remove: (name: string) => Promise<void>;
|
|
29
|
-
keep_automatic: (to_keep: number, interval_in_milliseconds?: number, now?: Date) => Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
export declare function backups(naming: BackupNaming, store: BackupStore): Backups;
|
package/dist/backups.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Named backups of an app's data: the name says when each was taken and whether it was automatic, and daily ones keep a set number.
|
|
2
|
-
const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
-
// Names such as tasks-2026-06-10T09-30-00Z-auto.db for the prefix "tasks" and the extension ".db".
|
|
4
|
-
export function backup_naming(prefix, extension) {
|
|
5
|
-
const pattern = new RegExp(`^${escaped(prefix)}-(\\d{4}-\\d{2}-\\d{2})T(\\d{2})-(\\d{2})-(\\d{2})Z(-auto)?${escaped(extension)}$`);
|
|
6
|
-
return {
|
|
7
|
-
pattern,
|
|
8
|
-
name_for: (taken_at, is_automatic) => `${prefix}-${taken_at.toISOString().slice(0, 19).replaceAll(":", "-")}Z${is_automatic ? "-auto" : ""}${extension}`,
|
|
9
|
-
describe: (name, size_bytes) => {
|
|
10
|
-
const match = pattern.exec(name);
|
|
11
|
-
if (!match)
|
|
12
|
-
throw new Error(`not a backup name: ${name}`);
|
|
13
|
-
const [, date, hours, minutes, seconds, automatic] = match;
|
|
14
|
-
return { name, taken_at: `${date}T${hours}:${minutes}:${seconds}Z`, is_automatic: automatic !== undefined, size_bytes };
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
const day_in_milliseconds = 24 * 60 * 60 * 1000;
|
|
19
|
-
export function backups(naming, store) {
|
|
20
|
-
const list = async () => (await store.list())
|
|
21
|
-
.filter((file) => naming.pattern.test(file.name))
|
|
22
|
-
.map((file) => naming.describe(file.name, file.size_bytes))
|
|
23
|
-
.sort((first, second) => second.taken_at.localeCompare(first.taken_at));
|
|
24
|
-
const remove = async (name) => {
|
|
25
|
-
if (!naming.pattern.test(name))
|
|
26
|
-
throw new Error(`not a backup name: ${name}`);
|
|
27
|
-
await store.remove(name);
|
|
28
|
-
};
|
|
29
|
-
const create = async (is_automatic = false, now = new Date()) => {
|
|
30
|
-
const name = naming.name_for(now, is_automatic);
|
|
31
|
-
const existing = (await list()).find((backup) => backup.name === name);
|
|
32
|
-
return existing ?? naming.describe(name, await store.take(name));
|
|
33
|
-
};
|
|
34
|
-
const keep_automatic = async (to_keep, interval_in_milliseconds = day_in_milliseconds, now = new Date()) => {
|
|
35
|
-
const newest = (await list()).find((backup) => backup.is_automatic);
|
|
36
|
-
if (!newest || now.getTime() - Date.parse(newest.taken_at) >= interval_in_milliseconds)
|
|
37
|
-
await create(true, now);
|
|
38
|
-
const automatic = (await list()).filter((backup) => backup.is_automatic);
|
|
39
|
-
for (const backup of automatic.slice(Math.max(0, to_keep)))
|
|
40
|
-
await remove(backup.name);
|
|
41
|
-
};
|
|
42
|
-
return { naming, list, create: (is_automatic) => create(is_automatic), remove, keep_automatic };
|
|
43
|
-
}
|
package/dist/dates.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export declare function has_time(local_date: string): boolean;
|
|
2
|
-
export declare function parse_local(local_date: string): Date;
|
|
3
|
-
export declare function format_local(date: Date, with_time: boolean): string;
|
|
4
|
-
export declare function start_of_day(date: Date): Date;
|
|
5
|
-
export declare function days_between(first: Date, second: Date): number;
|
|
6
|
-
export declare function add_days(date: Date, days: number): Date;
|
|
7
|
-
export declare function add_months(date: Date, months: number): Date;
|
|
8
|
-
export declare function is_overdue(local_date: string, now?: Date): boolean;
|
|
9
|
-
export declare function relative_day_label(local_date: string, now?: Date, locale?: string): string;
|
package/dist/dates.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Local calendar dates written as "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM", read and written in the device's own time zone.
|
|
2
|
-
const DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
|
|
3
|
-
// ── Text and dates ───────────────────────────────────────────────────────────
|
|
4
|
-
export function has_time(local_date) {
|
|
5
|
-
return local_date.includes("T");
|
|
6
|
-
}
|
|
7
|
-
// A date without a time stands for midnight at the start of its day.
|
|
8
|
-
export function parse_local(local_date) {
|
|
9
|
-
const [day = "", time = "00:00"] = local_date.split("T");
|
|
10
|
-
const [year = 0, month = 1, day_of_month = 1] = day.split("-").map(Number);
|
|
11
|
-
const [hours = 0, minutes = 0] = time.split(":").map(Number);
|
|
12
|
-
return new Date(year, month - 1, day_of_month, hours, minutes);
|
|
13
|
-
}
|
|
14
|
-
export function format_local(date, with_time) {
|
|
15
|
-
const pad = (number) => String(number).padStart(2, "0");
|
|
16
|
-
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
17
|
-
return with_time ? `${day}T${pad(date.getHours())}:${pad(date.getMinutes())}` : day;
|
|
18
|
-
}
|
|
19
|
-
// ── Arithmetic ───────────────────────────────────────────────────────────────
|
|
20
|
-
export function start_of_day(date) {
|
|
21
|
-
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
22
|
-
}
|
|
23
|
-
// Whole calendar days from the first date to the second, so a clock change in between still counts as one day.
|
|
24
|
-
export function days_between(first, second) {
|
|
25
|
-
return Math.round((start_of_day(second).getTime() - start_of_day(first).getTime()) / DAY_IN_MILLISECONDS);
|
|
26
|
-
}
|
|
27
|
-
export function add_days(date, days) {
|
|
28
|
-
const moved = new Date(date);
|
|
29
|
-
moved.setDate(moved.getDate() + days);
|
|
30
|
-
return moved;
|
|
31
|
-
}
|
|
32
|
-
// A day past the end of the target month falls back to its last day, so Jan 31 plus a month is Feb 28.
|
|
33
|
-
export function add_months(date, months) {
|
|
34
|
-
const last_day = new Date(date.getFullYear(), date.getMonth() + months + 1, 0).getDate();
|
|
35
|
-
return new Date(date.getFullYear(), date.getMonth() + months, Math.min(date.getDate(), last_day), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
36
|
-
}
|
|
37
|
-
// ── Labels ───────────────────────────────────────────────────────────────────
|
|
38
|
-
// A date without a time is overdue only once its day has passed.
|
|
39
|
-
export function is_overdue(local_date, now = new Date()) {
|
|
40
|
-
const date = parse_local(local_date);
|
|
41
|
-
return has_time(local_date) ? date < now : date < start_of_day(now);
|
|
42
|
-
}
|
|
43
|
-
// Yesterday, Today or Tomorrow in the reader's language, else a short date with the year only when it is not this year, then the time if there is one.
|
|
44
|
-
export function relative_day_label(local_date, now = new Date(), locale) {
|
|
45
|
-
const date = parse_local(local_date);
|
|
46
|
-
const day_offset = days_between(now, date);
|
|
47
|
-
const day_label = Math.abs(day_offset) <= 1
|
|
48
|
-
? capitalised(new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(day_offset, "day"), locale)
|
|
49
|
-
: date.toLocaleDateString(locale, {
|
|
50
|
-
month: "short",
|
|
51
|
-
day: "numeric",
|
|
52
|
-
year: date.getFullYear() === now.getFullYear() ? undefined : "numeric",
|
|
53
|
-
});
|
|
54
|
-
if (!has_time(local_date))
|
|
55
|
-
return day_label;
|
|
56
|
-
return `${day_label} ${date.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit" })}`;
|
|
57
|
-
}
|
|
58
|
-
function capitalised(text, locale) {
|
|
59
|
-
return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
|
|
60
|
-
}
|
package/dist/durations.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
declare const unit_names: readonly [readonly ["months", 2592000], readonly ["weeks", 604800], readonly ["days", 86400], readonly ["hours", 3600], readonly ["minutes", 60], readonly ["seconds", 1]];
|
|
2
|
-
export type DurationUnit = (typeof unit_names)[number][0];
|
|
3
|
-
export interface DurationWording {
|
|
4
|
-
separator?: string;
|
|
5
|
-
largest_unit?: DurationUnit;
|
|
6
|
-
}
|
|
7
|
-
export declare function parse_duration(value: unknown, key: string): number;
|
|
8
|
-
export declare function format_duration(seconds: number, wording?: DurationWording): string;
|
|
9
|
-
export declare function format_age(seconds: number): string;
|
|
10
|
-
export declare function format_clock(milliseconds: number | null | undefined, empty?: string): string;
|
|
11
|
-
export {};
|
package/dist/durations.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Reads durations such as "2h30m" from settings and writes them back in words, for the server and the agent alike.
|
|
2
|
-
import parse from "parse-duration";
|
|
3
|
-
// A month counts as 30 days.
|
|
4
|
-
const unit_names = [["months", 2592000], ["weeks", 604800], ["days", 86400], ["hours", 3600], ["minutes", 60], ["seconds", 1]];
|
|
5
|
-
const words = new Intl.DurationFormat("en", { style: "long" });
|
|
6
|
-
// A duration in whole seconds; `key` names the setting in the error.
|
|
7
|
-
export function parse_duration(value, key) {
|
|
8
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
9
|
-
return Math.floor(value);
|
|
10
|
-
const text = String(value).trim();
|
|
11
|
-
// A bare number is seconds, where parse-duration would read milliseconds.
|
|
12
|
-
const seconds = /^\d+$/.test(text) ? Number(text) : parse(text, "s");
|
|
13
|
-
if (seconds === null || seconds === undefined || !Number.isFinite(seconds) || seconds < 0) {
|
|
14
|
-
throw new Error(`\`${key}\` must be a duration such as "1w", "7d", "2h30m" or "60s", not ${JSON.stringify(value)}`);
|
|
15
|
-
}
|
|
16
|
-
return Math.floor(seconds);
|
|
17
|
-
}
|
|
18
|
-
// The two largest whole units, such as "1 day, 2 hours" or, with a space between, "1 week 2 days".
|
|
19
|
-
export function format_duration(seconds, wording = {}) {
|
|
20
|
-
if (seconds < 1)
|
|
21
|
-
return "0 seconds";
|
|
22
|
-
const first_unit = unit_names.findIndex(([name]) => name === (wording.largest_unit ?? "months"));
|
|
23
|
-
const parts = {};
|
|
24
|
-
let remaining = Math.floor(seconds);
|
|
25
|
-
for (const [name, size] of unit_names.slice(first_unit)) {
|
|
26
|
-
if (Object.keys(parts).length === 2)
|
|
27
|
-
break;
|
|
28
|
-
if (remaining < size)
|
|
29
|
-
continue;
|
|
30
|
-
parts[name] = Math.floor(remaining / size);
|
|
31
|
-
remaining %= size;
|
|
32
|
-
}
|
|
33
|
-
if (wording.separator === undefined)
|
|
34
|
-
return words.format(parts);
|
|
35
|
-
return Object.entries(parts).map(([name, count]) => words.format({ [name]: count })).join(wording.separator);
|
|
36
|
-
}
|
|
37
|
-
export function format_age(seconds) {
|
|
38
|
-
return seconds < 0 ? "never" : `${format_duration(seconds)} ago`;
|
|
39
|
-
}
|
|
40
|
-
// A playing time as a clock, such as "3:07", or "1:02:03" from an hour.
|
|
41
|
-
export function format_clock(milliseconds, empty = "0:00") {
|
|
42
|
-
if (milliseconds === null || milliseconds === undefined || !Number.isFinite(milliseconds))
|
|
43
|
-
return empty;
|
|
44
|
-
const total = Math.max(0, Math.round(milliseconds / 1000));
|
|
45
|
-
const hours = Math.floor(total / 3600);
|
|
46
|
-
const minutes = Math.floor((total % 3600) / 60);
|
|
47
|
-
const seconds = String(total % 60).padStart(2, "0");
|
|
48
|
-
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
49
|
-
}
|
package/dist/electron/app.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface AppLike {
|
|
2
|
-
requestSingleInstanceLock(): boolean;
|
|
3
|
-
quit(): void;
|
|
4
|
-
on(event: "second-instance", listener: (event: unknown, argv: string[]) => void): unknown;
|
|
5
|
-
}
|
|
6
|
-
export interface FocusableWindowLike {
|
|
7
|
-
isMinimized(): boolean;
|
|
8
|
-
restore(): void;
|
|
9
|
-
focus(): void;
|
|
10
|
-
}
|
|
11
|
-
export declare function hold_single_instance(app: AppLike, on_second_launch: (argv: string[]) => void): boolean;
|
|
12
|
-
export declare function bring_to_front(window: FocusableWindowLike): void;
|
|
13
|
-
export declare function first_argument<Value>(argv: readonly string[], parse: (argument: string) => Value | null): Value | null;
|
package/dist/electron/app.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// The app's place on the desktop: one copy running at a time, its window brought forward, and links handed over on the command line.
|
|
2
|
-
// ── One copy ─────────────────────────────────────────────────────────────────
|
|
3
|
-
// Keeps one copy running: a second launch hands its command line to the first and quits; returns false in the copy that must quit.
|
|
4
|
-
export function hold_single_instance(app, on_second_launch) {
|
|
5
|
-
if (!app.requestSingleInstanceLock()) {
|
|
6
|
-
app.quit();
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
app.on("second-instance", (_event, argv) => on_second_launch(argv));
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
// Brings a window forward, restoring it first when minimised.
|
|
13
|
-
export function bring_to_front(window) {
|
|
14
|
-
if (window.isMinimized())
|
|
15
|
-
window.restore();
|
|
16
|
-
window.focus();
|
|
17
|
-
}
|
|
18
|
-
// ── Command line ─────────────────────────────────────────────────────────────
|
|
19
|
-
// The first command-line argument a parser accepts, such as a task link, or null.
|
|
20
|
-
export function first_argument(argv, parse) {
|
|
21
|
-
for (const argument of argv) {
|
|
22
|
-
const value = parse(argument);
|
|
23
|
-
if (value !== null)
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
export interface IpcMainLike {
|
|
2
|
-
handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): void;
|
|
3
|
-
removeHandler(channel: string): void;
|
|
4
|
-
}
|
|
5
|
-
export interface IpcMainEventLike {
|
|
6
|
-
senderFrame?: {
|
|
7
|
-
url: string;
|
|
8
|
-
} | null;
|
|
9
|
-
}
|
|
10
|
-
export interface IpcRendererLike {
|
|
11
|
-
invoke(channel: string, ...args: unknown[]): Promise<unknown>;
|
|
12
|
-
}
|
|
13
|
-
export interface ContextBridgeLike {
|
|
14
|
-
exposeInMainWorld(api_key: string, api: unknown): void;
|
|
15
|
-
}
|
|
16
|
-
type AnyMethod = (...args: never[]) => unknown;
|
|
17
|
-
export type Client<Methods extends Record<keyof Methods, AnyMethod>> = {
|
|
18
|
-
[Name in keyof Methods]: (...args: Parameters<Methods[Name]>) => Promise<Awaited<ReturnType<Methods[Name]>>>;
|
|
19
|
-
};
|
|
20
|
-
export type Implementation<Methods extends Record<keyof Methods, AnyMethod>> = {
|
|
21
|
-
[Name in keyof Methods]: (...args: Parameters<Methods[Name]>) => Awaited<ReturnType<Methods[Name]>> | Promise<Awaited<ReturnType<Methods[Name]>>>;
|
|
22
|
-
};
|
|
23
|
-
export type CallRemote = (method: string, args: unknown[]) => Promise<unknown>;
|
|
24
|
-
export type Envelope<Result = unknown> = {
|
|
25
|
-
ok: true;
|
|
26
|
-
result: Result;
|
|
27
|
-
} | {
|
|
28
|
-
ok: false;
|
|
29
|
-
error: string;
|
|
30
|
-
};
|
|
31
|
-
export interface HandleOptions {
|
|
32
|
-
trusted?: (event: IpcMainEventLike) => boolean;
|
|
33
|
-
envelopes?: boolean;
|
|
34
|
-
}
|
|
35
|
-
export interface ClientOptions {
|
|
36
|
-
envelopes?: boolean;
|
|
37
|
-
}
|
|
38
|
-
export interface Bridge<Methods extends Record<keyof Methods, AnyMethod>> {
|
|
39
|
-
api_name: string;
|
|
40
|
-
method_names: readonly (keyof Methods & string)[];
|
|
41
|
-
channel: (method: keyof Methods & string) => string;
|
|
42
|
-
expose: (context_bridge: ContextBridgeLike, ipc_renderer: IpcRendererLike, options?: ClientOptions) => void;
|
|
43
|
-
ipc_client: (ipc_renderer: IpcRendererLike, options?: ClientOptions) => Client<Methods>;
|
|
44
|
-
handle: (ipc_main: IpcMainLike, implementation: Implementation<Methods>, options?: HandleOptions) => () => void;
|
|
45
|
-
web_rpc: (call_remote: CallRemote) => Client<Methods>;
|
|
46
|
-
dispatch: (implementation: Implementation<Methods>, method: string, args: unknown[]) => Promise<unknown>;
|
|
47
|
-
}
|
|
48
|
-
export declare function define_bridge<Methods extends Record<keyof Methods, AnyMethod>>(api_name: string, method_names: readonly (keyof Methods & string)[]): Bridge<Methods>;
|
|
49
|
-
export declare function to_envelope<Result>(work: () => Result | Promise<Result>): Promise<Envelope<Awaited<Result>>>;
|
|
50
|
-
export declare function clean_error_message(message: string): string;
|
|
51
|
-
export declare function clean_invoker(ipc_renderer: IpcRendererLike): IpcRendererLike;
|
|
52
|
-
export declare function unwrapping_invoker(ipc_renderer: IpcRendererLike): IpcRendererLike;
|
|
53
|
-
export declare function to_cloneable(value: unknown): unknown;
|
|
54
|
-
export declare function with_cloneable_arguments<Methods extends Record<keyof Methods, AnyMethod>>(client: Client<Methods>): Client<Methods>;
|
|
55
|
-
export {};
|