@pablopunk/tau 0.1.0 → 0.1.2
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 +1 -1
- package/dist/latest-version-di9hD7cY.js +106 -0
- package/dist/machines-behind-BjGgmSs_.js +257 -0
- package/dist/{main-CI9q691u.js → main-R5EDx89w.js} +349 -229
- package/dist/{service-command-C7WDeeR2.js → manager-B_WHbb1U.js} +1 -33
- package/dist/real-deps-f96UmLBz.js +89 -0
- package/dist/serve-CuMzEud7.js +1 -0
- package/dist/{serve-92lMbcls.js → serve-Uc4Nin5f.js} +25 -51
- package/dist/service-command-C74K0saj.js +34 -0
- package/dist/service-command-DqcstJ8e.js +1 -0
- package/dist/{wait-for-lock-D2V3vuye.js → src-DO9LUlhi.js} +183 -46
- package/dist/wait-for-lock-B1Qx8GWg.js +1 -0
- package/dist/wait-for-lock-CrSl2mi1.js +25 -0
- package/dist/web/dist/assets/{index-7m0crRNF.css → index-D_Aa3Oou.css} +1 -1
- package/dist/web/dist/assets/index-vDvuOoR3.js +110 -0
- package/dist/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/serve-BcVZj5e2.js +0 -1
- package/dist/service-command-BVr33vlP.js +0 -1
- package/dist/wait-for-lock-BMk6v3J5.js +0 -1
- package/dist/web/dist/assets/index--f7AYPm0.js +0 -108
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-
|
|
17
|
+
const { main } = await import("./main-R5EDx89w.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 };
|