@pablopunk/tau 0.1.0 → 0.1.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/bin.js CHANGED
@@ -14,7 +14,7 @@ function withoutSqliteWarning(emitWarning) {
14
14
  process.emitWarning = withoutSqliteWarning(process.emitWarning.bind(process));
15
15
  //#endregion
16
16
  //#region src/bin.ts
17
- const { main } = await import("./main-CI9q691u.js");
17
+ const { main } = await import("./main-D-Tg9b5Y.js");
18
18
  main(process.argv.slice(2)).catch((error) => {
19
19
  console.error(error instanceof Error ? error.message : error);
20
20
  process.exit(1);
@@ -0,0 +1,106 @@
1
+ import { l as isPlainVersion } from "./src-DO9LUlhi.js";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
6
+ import { spawn } from "node:child_process";
7
+ //#region src/private-files.ts
8
+ const PRIVATE_DIRECTORY_MODE = 448;
9
+ const PRIVATE_FILE_MODE = 384;
10
+ async function ensurePrivateDirectory(path) {
11
+ await mkdir(path, {
12
+ recursive: true,
13
+ mode: PRIVATE_DIRECTORY_MODE
14
+ });
15
+ }
16
+ /** Temp file, then rename, so readers never see a half-written file. */
17
+ async function writePrivateFile(path, content) {
18
+ await ensurePrivateDirectory(dirname(path));
19
+ const temporary = `${path}.${process.pid}.tmp`;
20
+ await writeFile(temporary, content, { mode: PRIVATE_FILE_MODE });
21
+ await rename(temporary, path);
22
+ }
23
+ //#endregion
24
+ //#region src/token.ts
25
+ const TOKEN_BYTES = 32;
26
+ const MIN_TOKEN_LENGTH = 32;
27
+ const tokenPath = (home) => join(home, "token");
28
+ async function readExistingToken(home) {
29
+ try {
30
+ const token = (await readFile(tokenPath(home), "utf8")).trim();
31
+ return token.length >= MIN_TOKEN_LENGTH ? token : void 0;
32
+ } catch {
33
+ return;
34
+ }
35
+ }
36
+ /** The token outlives restarts so open browser tabs keep their cookie. */
37
+ async function readOrCreateToken(home) {
38
+ const existing = await readExistingToken(home);
39
+ if (existing) return existing;
40
+ const token = randomBytes(TOKEN_BYTES).toString("base64url");
41
+ await writePrivateFile(tokenPath(home), `${token}\n`);
42
+ return token;
43
+ }
44
+ //#endregion
45
+ //#region src/version.ts
46
+ /** From the `package.json` next to `src/` and next to `dist/`. */
47
+ function cliVersion() {
48
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
49
+ }
50
+ //#endregion
51
+ //#region src/update/command-runner.ts
52
+ /** Spawns with an args array and no shell. A missing binary is a result with `code: null`. */
53
+ const spawnCommand = (command, args, options) => new Promise((resolve) => {
54
+ const child = spawn(command, [...args], {
55
+ env: options.env ?? process.env,
56
+ stdio: options.stdio === "inherit" ? "inherit" : [
57
+ "inherit",
58
+ options.stdio === "tee" ? "inherit" : "pipe",
59
+ "pipe"
60
+ ],
61
+ timeout: options.timeoutMs
62
+ });
63
+ let stdout = "";
64
+ let stderr = "";
65
+ child.stdout?.on("data", (chunk) => {
66
+ stdout += chunk.toString();
67
+ });
68
+ child.stderr?.on("data", (chunk) => {
69
+ stderr += chunk.toString();
70
+ if (options.stdio === "tee") process.stderr.write(chunk);
71
+ });
72
+ child.on("error", (error) => resolve({
73
+ code: null,
74
+ stdout,
75
+ stderr: stderr + error.message
76
+ }));
77
+ child.on("close", (code) => resolve({
78
+ code,
79
+ stdout,
80
+ stderr
81
+ }));
82
+ });
83
+ //#endregion
84
+ //#region src/update/latest-version.ts
85
+ const PACKAGE_NAME = "@pablopunk/tau";
86
+ const VIEW_TIMEOUT_MS = 15e3;
87
+ const npmOf = (execPath) => join(dirname(execPath), "npm");
88
+ /** The registry's `latest` as plain `x.y.z`; anything else is an error. */
89
+ async function fetchLatestVersion(run, execPath) {
90
+ const args = [
91
+ "view",
92
+ `${PACKAGE_NAME}@latest`,
93
+ "version",
94
+ "--prefer-online"
95
+ ];
96
+ const result = await run(npmOf(execPath), args, {
97
+ stdio: "capture",
98
+ timeoutMs: VIEW_TIMEOUT_MS
99
+ });
100
+ if (result.code !== 0) throw new Error(`Could not read the latest version from the npm registry (${result.stderr.trim().split("\n")[0] || `exit ${result.code}`}).`);
101
+ const version = result.stdout.trim();
102
+ if (!isPlainVersion(version)) throw new Error(`The registry returned an unexpected version: ${version}`);
103
+ return version;
104
+ }
105
+ //#endregion
106
+ export { cliVersion as a, writePrivateFile as c, spawnCommand as i, fetchLatestVersion as n, readOrCreateToken as o, npmOf as r, ensurePrivateDirectory as s, PACKAGE_NAME as t };
@@ -0,0 +1,257 @@
1
+ import { $ as routes, L as isErrorBody, Z as fillPath, c as isBehind, n as readRunningInstance } from "./src-DO9LUlhi.js";
2
+ import { a as cliVersion, o as readOrCreateToken } from "./latest-version-di9hD7cY.js";
3
+ import { i as realContext, t as createServiceManager } from "./manager-B_WHbb1U.js";
4
+ import { t as cliEntry } from "./cli-entry-DcDpQ30L.js";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { promisify } from "node:util";
8
+ import { readFile } from "node:fs/promises";
9
+ import { execFile } from "node:child_process";
10
+ /** A failure that ends the command with a message on stderr and an exit code. */
11
+ var CliExit = class extends Error {
12
+ exitCode;
13
+ constructor(exitCode, message) {
14
+ super(message);
15
+ this.name = "CliExit";
16
+ this.exitCode = exitCode;
17
+ }
18
+ };
19
+ const usageError = (message) => new CliExit(2, message);
20
+ const failure = (message) => new CliExit(1, message);
21
+ const NOT_RUNNING_MESSAGE = "tau is not running. Run `tau` to start it.";
22
+ //#endregion
23
+ //#region src/commands/client.ts
24
+ const URL_POLL_MS = 100;
25
+ async function waitForLockUrl(home, env) {
26
+ const deadline = env.now() + env.startupWaitMs;
27
+ for (;;) {
28
+ const info = await readRunningInstance(home);
29
+ if (!info) throw failure(NOT_RUNNING_MESSAGE);
30
+ if (info.url) return info.url;
31
+ if (env.now() >= deadline) throw failure("tau is starting but has not published its URL yet.");
32
+ await env.sleep(URL_POLL_MS);
33
+ }
34
+ }
35
+ async function readToken(home) {
36
+ try {
37
+ return (await readFile(join(home, "token"), "utf8")).trim();
38
+ } catch {
39
+ throw failure(NOT_RUNNING_MESSAGE);
40
+ }
41
+ }
42
+ /** The lock gives the URL and `<home>/token` the secret. Retries while a live server has no URL yet. */
43
+ async function discoverServer(env) {
44
+ return {
45
+ url: await waitForLockUrl(env.home, env),
46
+ token: await readToken(env.home)
47
+ };
48
+ }
49
+ const queryString = (query) => {
50
+ const entries = Object.entries(query ?? {});
51
+ return entries.length === 0 ? "" : `?${new URLSearchParams(entries).toString()}`;
52
+ };
53
+ async function errorMessageOf(response) {
54
+ const text = await response.text();
55
+ try {
56
+ const parsed = JSON.parse(text);
57
+ if (isErrorBody(parsed)) return `${parsed.error.code}: ${parsed.error.message}`;
58
+ } catch {}
59
+ return `HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`;
60
+ }
61
+ async function send(server, name, request, env) {
62
+ const route = routes[name];
63
+ const target = `${server.url}${fillPath(route.path, request.params)}${queryString(request.query)}`;
64
+ const hasBody = request.body !== void 0;
65
+ try {
66
+ return await env.fetch(target, {
67
+ method: route.method,
68
+ headers: {
69
+ authorization: `Bearer ${server.token}`,
70
+ ...hasBody ? { "content-type": "application/json" } : {}
71
+ },
72
+ body: hasBody ? JSON.stringify(request.body) : void 0
73
+ });
74
+ } catch {
75
+ throw failure(NOT_RUNNING_MESSAGE);
76
+ }
77
+ }
78
+ /** Calls one route. Returns the parsed JSON, or `undefined` for an empty answer. An `AppError` becomes a failure. */
79
+ async function callRoute(server, name, request, env) {
80
+ const response = await send(server, name, request, env);
81
+ if (!response.ok) throw failure(await errorMessageOf(response));
82
+ const text = await response.text();
83
+ return text ? JSON.parse(text) : void 0;
84
+ }
85
+ //#endregion
86
+ //#region src/commands/command-env.ts
87
+ const defaultOpenSocket = (url, headers) => {
88
+ return new WebSocket(url, { headers });
89
+ };
90
+ const homeOf = (env) => env.TAU_HOME ?? join(homedir(), ".tau");
91
+ function processEnv() {
92
+ return {
93
+ home: homeOf(process.env),
94
+ env: process.env,
95
+ stdout: (text) => void process.stdout.write(`${text}\n`),
96
+ stderr: (text) => void process.stderr.write(`${text}\n`),
97
+ fetch: (input, init) => fetch(input, init),
98
+ openSocket: defaultOpenSocket,
99
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
100
+ after: (ms, fn) => {
101
+ const timer = setTimeout(fn, ms);
102
+ return () => clearTimeout(timer);
103
+ },
104
+ now: Date.now,
105
+ startupWaitMs: 5e3,
106
+ pollMs: 2e3,
107
+ skillPaths: []
108
+ };
109
+ }
110
+ //#endregion
111
+ //#region src/lifecycle/status-extras.ts
112
+ const NO_EXTRAS = {
113
+ uptimeSeconds: null,
114
+ lastExit: null,
115
+ tunnel: null
116
+ };
117
+ const execFileAsync = promisify(execFile);
118
+ /** `[[dd-]hh:]mm:ss` as printed by `ps -o etime=`. */
119
+ function parseElapsed(text) {
120
+ const match = /^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/.exec(text.trim());
121
+ if (!match) return null;
122
+ const [, days, hours, minutes, seconds] = match.map((part) => Number(part ?? 0));
123
+ return ((days ?? 0) * 24 + (hours ?? 0)) * 3600 + (minutes ?? 0) * 60 + (seconds ?? 0);
124
+ }
125
+ async function uptimeOf(pid) {
126
+ try {
127
+ const { stdout } = await execFileAsync("ps", [
128
+ "-o",
129
+ "etime=",
130
+ "-p",
131
+ String(pid)
132
+ ]);
133
+ return parseElapsed(stdout);
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ /** Missing or corrupt files give `null`. */
139
+ async function readLastExit(home) {
140
+ try {
141
+ const value = JSON.parse(await readFile(join(home, "last-exit.json"), "utf8"));
142
+ if (typeof value.reason !== "string" || typeof value.at !== "number") return null;
143
+ return {
144
+ reason: value.reason,
145
+ at: value.at,
146
+ ...value.detail ? { detail: String(value.detail) } : {}
147
+ };
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ async function readTunnel(home, instance) {
153
+ if (!instance?.url) return null;
154
+ try {
155
+ const token = (await readFile(join(home, "token"), "utf8")).trim();
156
+ const response = await fetch(`${instance.url}/api/remote`, {
157
+ headers: { authorization: `Bearer ${token}` },
158
+ signal: AbortSignal.timeout(2e3)
159
+ });
160
+ if (!response.ok) return null;
161
+ const { state, url, message } = await response.json();
162
+ return {
163
+ state,
164
+ url,
165
+ message
166
+ };
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+ async function realExtras(home, instance) {
172
+ const [uptimeSeconds, lastExit, tunnel] = await Promise.all([
173
+ instance ? uptimeOf(instance.pid) : null,
174
+ readLastExit(home),
175
+ readTunnel(home, instance)
176
+ ]);
177
+ return {
178
+ uptimeSeconds,
179
+ lastExit,
180
+ tunnel
181
+ };
182
+ }
183
+ function formatDuration(totalSeconds) {
184
+ const days = Math.floor(totalSeconds / 86400);
185
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
186
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
187
+ if (days > 0) return `${days}d ${hours}h`;
188
+ if (hours > 0) return `${hours}h ${minutes}m`;
189
+ return minutes > 0 ? `${minutes}m ${totalSeconds % 60}s` : `${totalSeconds}s`;
190
+ }
191
+ function renderExtras(extras) {
192
+ const { uptimeSeconds, lastExit, tunnel } = extras;
193
+ const exit = lastExit ? `${lastExit.reason}${lastExit.detail ? ` (${lastExit.detail})` : ""} at ${new Date(lastExit.at).toISOString()}` : "-";
194
+ const tunnelText = tunnel ? `${tunnel.state}${tunnel.url ? ` ${tunnel.url}` : ""}` : "-";
195
+ return [
196
+ `uptime: ${uptimeSeconds === null ? "-" : formatDuration(uptimeSeconds)}`,
197
+ `last exit: ${exit}`,
198
+ `tunnel: ${tunnelText}`
199
+ ];
200
+ }
201
+ //#endregion
202
+ //#region src/lifecycle/deps.ts
203
+ const withToken = (origin, token) => `${origin}/?token=${encodeURIComponent(token)}`;
204
+ const managerOf = (deps, home) => createServiceManager(deps.context, deps.cli(), home);
205
+ const NOT_RUNNING = "tau is not running. Run `tau` to start it.";
206
+ async function realLifecycleDeps() {
207
+ const { waitForUrl } = await import("./wait-for-lock-B1Qx8GWg.js");
208
+ const { existsSync } = await import("node:fs");
209
+ return {
210
+ context: realContext(),
211
+ cli: () => cliEntry(),
212
+ version: cliVersion,
213
+ readInstance: readRunningInstance,
214
+ waitForInstanceUrl: (home) => waitForUrl(home),
215
+ readToken: readOrCreateToken,
216
+ openBrowser: async (url) => {
217
+ const { default: open } = await import("open");
218
+ await open(url);
219
+ },
220
+ out: (message) => console.log(message),
221
+ fileExists: existsSync,
222
+ extras: realExtras
223
+ };
224
+ }
225
+ //#endregion
226
+ //#region src/update/machines-behind.ts
227
+ /** Best effort: `undefined` when no server runs or any request fails. */
228
+ async function fetchLinkedMachines(home) {
229
+ try {
230
+ const env = {
231
+ ...processEnv(),
232
+ home
233
+ };
234
+ const server = await discoverServer(env);
235
+ const machines = await callRoute(server, "listMachines", {}, env);
236
+ if (machines.length === 0) return void 0;
237
+ return {
238
+ machines,
239
+ hubLatest: (await callRoute(server, "getUpdateStatus", {}, env)).latest
240
+ };
241
+ } catch {
242
+ return;
243
+ }
244
+ }
245
+ /** The "Linked machines behind" line, or undefined when nothing is behind or worth saying. */
246
+ function linkedMachinesLine(linked, fallbackLatest) {
247
+ if (!linked) return void 0;
248
+ const latest = linked.hubLatest ?? fallbackLatest;
249
+ const items = linked.machines.filter((m) => !m.linkedTo).flatMap((m) => {
250
+ const behind = isBehind(m.tauVersion, latest);
251
+ if (behind === void 0) return [`${m.name} (version unknown)`];
252
+ return behind ? [`${m.name} (${m.tauVersion})`] : [];
253
+ });
254
+ return items.length === 0 ? void 0 : `Linked machines behind: ${items.join(", ")}`;
255
+ }
256
+ //#endregion
257
+ export { realLifecycleDeps as a, renderExtras as c, discoverServer as d, CliExit as f, managerOf as i, processEnv as l, usageError as m, linkedMachinesLine as n, withToken as o, failure as p, NOT_RUNNING as r, NO_EXTRAS as s, fetchLinkedMachines as t, callRoute as u };