sublera-cli 0.0.0-stage → 1.3.0

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/LICENSE +21 -0
  3. package/README.md +585 -2
  4. package/SECURITY.md +78 -0
  5. package/THIRD_PARTY_NOTICES.md +37 -0
  6. package/dist/agent.d.ts +270 -0
  7. package/dist/agent.js +1867 -0
  8. package/dist/commands.d.ts +76 -0
  9. package/dist/commands.js +841 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +95 -0
  12. package/dist/engine/binaryManager.d.ts +67 -0
  13. package/dist/engine/binaryManager.js +590 -0
  14. package/dist/engine/gguf.d.ts +88 -0
  15. package/dist/engine/gguf.js +458 -0
  16. package/dist/engine/llamaRunner.d.ts +78 -0
  17. package/dist/engine/llamaRunner.js +282 -0
  18. package/dist/engine/modelManager.d.ts +63 -0
  19. package/dist/engine/modelManager.js +510 -0
  20. package/dist/engine/selfCheck.d.ts +34 -0
  21. package/dist/engine/selfCheck.js +108 -0
  22. package/dist/exitCodes.d.ts +20 -0
  23. package/dist/exitCodes.js +20 -0
  24. package/dist/hardware/gpuMonitor.d.ts +32 -0
  25. package/dist/hardware/gpuMonitor.js +378 -0
  26. package/dist/hardware/sleepGuard.d.ts +42 -0
  27. package/dist/hardware/sleepGuard.js +149 -0
  28. package/dist/hardware/types.d.ts +40 -0
  29. package/dist/hardware/types.js +2 -0
  30. package/dist/health.d.ts +17 -0
  31. package/dist/health.js +38 -0
  32. package/dist/index.d.ts +11 -0
  33. package/dist/index.js +268 -0
  34. package/dist/logger.d.ts +53 -0
  35. package/dist/logger.js +201 -0
  36. package/dist/network/proxy.d.ts +58 -0
  37. package/dist/network/proxy.js +328 -0
  38. package/dist/network/streamProxy.d.ts +51 -0
  39. package/dist/network/streamProxy.js +286 -0
  40. package/dist/network/tunnelClient.d.ts +54 -0
  41. package/dist/network/tunnelClient.js +257 -0
  42. package/dist/settings.d.ts +53 -0
  43. package/dist/settings.js +171 -0
  44. package/dist/state.d.ts +52 -0
  45. package/dist/state.js +71 -0
  46. package/dist/storageCommand.d.ts +22 -0
  47. package/dist/storageCommand.js +81 -0
  48. package/dist/types.d.ts +222 -0
  49. package/dist/types.js +43 -0
  50. package/dist/ui/progress.d.ts +29 -0
  51. package/dist/ui/progress.js +82 -0
  52. package/dist/ui/terminalDashboard.d.ts +88 -0
  53. package/dist/ui/terminalDashboard.js +295 -0
  54. package/dist/util/env.d.ts +3 -0
  55. package/dist/util/env.js +6 -0
  56. package/dist/util/exec.d.ts +23 -0
  57. package/dist/util/exec.js +116 -0
  58. package/dist/util/format.d.ts +47 -0
  59. package/dist/util/format.js +125 -0
  60. package/dist/util/fs.d.ts +18 -0
  61. package/dist/util/fs.js +140 -0
  62. package/dist/util/http.d.ts +28 -0
  63. package/dist/util/http.js +128 -0
  64. package/dist/util/instanceLock.d.ts +3 -0
  65. package/dist/util/instanceLock.js +108 -0
  66. package/dist/util/open.d.ts +16 -0
  67. package/dist/util/open.js +65 -0
  68. package/dist/util/storage.d.ts +10 -0
  69. package/dist/util/storage.js +69 -0
  70. package/dist/version.d.ts +15 -0
  71. package/dist/version.js +29 -0
  72. package/package.json +68 -4
@@ -0,0 +1,140 @@
1
+ import { appendFile, mkdir, open, readFile, rename, rm, stat, statfs } from 'node:fs/promises';
2
+ import { dirname, resolve, sep } from 'node:path';
3
+ import { createReadStream } from 'node:fs';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { configPath } from '../config.js';
6
+ export async function ensureDir(path, mode = 0o700) {
7
+ await mkdir(path, { recursive: true, mode });
8
+ }
9
+ export async function pathExists(path) {
10
+ try {
11
+ await stat(path);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export async function fileSize(path) {
19
+ try {
20
+ return (await stat(path)).size;
21
+ }
22
+ catch {
23
+ return 0;
24
+ }
25
+ }
26
+ export async function removeIfExists(path) {
27
+ await rm(path, { force: true, recursive: true });
28
+ }
29
+ /** Bytes available to this user on the disk that holds `dir`, or null when the OS cannot tell. */
30
+ export async function freeDiskBytes(dir) {
31
+ try {
32
+ const info = await statfs(dir);
33
+ return info.bavail * info.bsize;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ const pendingWrites = new Map();
40
+ /** Serialize same-process writers; each replacement still uses a distinct, synced temporary file. */
41
+ export async function atomicWrite(path, data, mode = 0o600) {
42
+ const key = process.platform === 'win32' ? resolve(path).toLowerCase() : resolve(path);
43
+ const previous = pendingWrites.get(key) ?? Promise.resolve();
44
+ const write = previous.catch(() => undefined).then(() => atomicReplace(path, data, mode));
45
+ pendingWrites.set(key, write);
46
+ try {
47
+ await write;
48
+ }
49
+ finally {
50
+ if (pendingWrites.get(key) === write)
51
+ pendingWrites.delete(key);
52
+ }
53
+ }
54
+ async function atomicReplace(path, data, mode) {
55
+ await ensureDir(dirname(path));
56
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
57
+ const handle = await open(tmp, 'wx', mode);
58
+ try {
59
+ await handle.writeFile(data, 'utf8');
60
+ await handle.sync();
61
+ await handle.close();
62
+ await rename(tmp, path);
63
+ }
64
+ finally {
65
+ await handle.close().catch(() => undefined);
66
+ await rm(tmp, { force: true }).catch(() => undefined);
67
+ }
68
+ }
69
+ export async function readConfigFile(path = configPath()) {
70
+ try {
71
+ const raw = await readFile(path, 'utf8');
72
+ const data = JSON.parse(raw);
73
+ if (!data || typeof data.server !== 'string' || typeof data.token !== 'string')
74
+ return null;
75
+ return {
76
+ server: data.server,
77
+ token: data.token,
78
+ wsPath: typeof data.wsPath === 'string' && data.wsPath ? data.wsPath : '/tunnel',
79
+ updatedAt: typeof data.updatedAt === 'number' ? data.updatedAt : 0,
80
+ tokenLabel: typeof data.tokenLabel === 'string' ? data.tokenLabel : undefined,
81
+ };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ export async function writeConfigFile(config, path = configPath()) {
88
+ await atomicWrite(path, `${JSON.stringify(config, null, 2)}\n`);
89
+ }
90
+ export async function logLine(file, line) {
91
+ await ensureDir(dirname(file));
92
+ await appendFile(file, `${new Date().toISOString()} ${line}\n`, 'utf8');
93
+ }
94
+ export function sha256Hex(input) {
95
+ return createHash('sha256').update(input).digest('hex');
96
+ }
97
+ export async function sha256File(path, onProgress) {
98
+ const total = await fileSize(path);
99
+ const hash = createHash('sha256');
100
+ let read = 0;
101
+ await new Promise((resolvePromise, reject) => {
102
+ const stream = createReadStream(path);
103
+ stream.on('data', (chunk) => {
104
+ hash.update(chunk);
105
+ read += chunk.length;
106
+ onProgress?.(read, total);
107
+ });
108
+ stream.on('end', () => resolvePromise());
109
+ stream.on('error', reject);
110
+ });
111
+ return hash.digest('hex');
112
+ }
113
+ export async function readFirstBytes(path, length) {
114
+ const handle = await import('node:fs/promises').then((fs) => fs.open(path, 'r'));
115
+ try {
116
+ const buf = Buffer.alloc(length);
117
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
118
+ return buf.subarray(0, bytesRead);
119
+ }
120
+ finally {
121
+ await handle.close();
122
+ }
123
+ }
124
+ /** Refuse paths that escape a directory, e.g. from a malicious archive. */
125
+ export function safeJoin(root, candidate) {
126
+ const full = resolve(root, candidate);
127
+ const rootWithSep = resolve(root) + sep;
128
+ if (full !== resolve(root) && !full.startsWith(rootWithSep)) {
129
+ throw new Error(`Archive entry escapes the install directory: ${candidate}`);
130
+ }
131
+ return full;
132
+ }
133
+ export function shortPath(path, max = 52) {
134
+ if (path.length <= max)
135
+ return path;
136
+ const head = Math.max(8, Math.floor(max * 0.35));
137
+ const tail = max - head - 1;
138
+ return `${path.slice(0, head)}…${path.slice(-tail)}`;
139
+ }
140
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1,28 @@
1
+ export type FetchJsonOptions = {
2
+ timeoutMs?: number;
3
+ headers?: Record<string, string>;
4
+ method?: string;
5
+ body?: string;
6
+ token?: string;
7
+ };
8
+ export declare class HttpError extends Error {
9
+ readonly status: number;
10
+ readonly body: string;
11
+ constructor(status: number, body: string, message?: string);
12
+ }
13
+ export declare function fetchJson<T>(url: string, options?: FetchJsonOptions): Promise<T>;
14
+ export declare function fetchText(url: string, options?: FetchJsonOptions): Promise<string>;
15
+ export declare function joinUrl(base: string, path: string): string;
16
+ /** Latest agent package on the npm registry, or null when it cannot be checked. */
17
+ export declare function latestPublishedVersion(timeoutMs?: number): Promise<string | null>;
18
+ export type SpeedSample = {
19
+ bytes: number;
20
+ ms: number;
21
+ bytesPerSec: number;
22
+ };
23
+ /** Downloads at most maxBytes for at most maxMs and returns the throughput after the response headers. */
24
+ export declare function measureDownloadSpeed(url: string, options?: {
25
+ maxBytes?: number;
26
+ maxMs?: number;
27
+ headerTimeoutMs?: number;
28
+ }): Promise<SpeedSample>;
@@ -0,0 +1,128 @@
1
+ import { toError } from './exec.js';
2
+ import { AGENT_NAME, USER_AGENT, outdatedMessage } from '../version.js';
3
+ export class HttpError extends Error {
4
+ status;
5
+ body;
6
+ constructor(status, body, message) {
7
+ super(message || `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ''}`);
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ }
12
+ export async function fetchJson(url, options = {}) {
13
+ const headers = {
14
+ accept: 'application/json',
15
+ 'user-agent': USER_AGENT,
16
+ ...options.headers,
17
+ };
18
+ if (options.token)
19
+ headers.authorization = `Bearer ${options.token}`;
20
+ if (options.body != null)
21
+ headers['content-type'] = 'application/json';
22
+ let response;
23
+ try {
24
+ response = await fetch(url, {
25
+ method: options.method || (options.body ? 'POST' : 'GET'),
26
+ headers,
27
+ body: options.body,
28
+ signal: AbortSignal.timeout(options.timeoutMs ?? 15_000),
29
+ });
30
+ }
31
+ catch (error) {
32
+ throw new Error(`Could not reach ${url}: ${toError(error).message}`);
33
+ }
34
+ const text = await response.text();
35
+ if (!response.ok) {
36
+ let message = text.slice(0, 300);
37
+ let minVersion;
38
+ try {
39
+ const parsed = JSON.parse(text);
40
+ message = typeof parsed.error === 'string' ? parsed.error : parsed.error?.message || parsed.message || message;
41
+ minVersion = typeof parsed.minVersion === 'string' ? parsed.minVersion : undefined;
42
+ }
43
+ catch {
44
+ /* raw text */
45
+ }
46
+ if (response.status === 426)
47
+ message = outdatedMessage(minVersion);
48
+ throw new HttpError(response.status, text, message);
49
+ }
50
+ if (!text)
51
+ return {};
52
+ try {
53
+ return JSON.parse(text);
54
+ }
55
+ catch {
56
+ throw new Error(`Expected JSON from ${url}`);
57
+ }
58
+ }
59
+ export async function fetchText(url, options = {}) {
60
+ const response = await fetch(url, {
61
+ headers: {
62
+ 'user-agent': USER_AGENT,
63
+ ...options.headers,
64
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
65
+ },
66
+ signal: AbortSignal.timeout(options.timeoutMs ?? 20_000),
67
+ });
68
+ const text = await response.text();
69
+ if (!response.ok)
70
+ throw new HttpError(response.status, text);
71
+ return text;
72
+ }
73
+ export function joinUrl(base, path) {
74
+ return `${base.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
75
+ }
76
+ /** Latest agent package on the npm registry, or null when it cannot be checked. */
77
+ export async function latestPublishedVersion(timeoutMs = 5_000) {
78
+ const registry = (process.env.npm_config_registry || 'https://registry.npmjs.org/').replace(/\/+$/, '');
79
+ try {
80
+ const body = await fetchJson(`${registry}/${AGENT_NAME}/latest`, { timeoutMs });
81
+ return typeof body.version === 'string' ? body.version : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Downloads at most maxBytes for at most maxMs and returns the throughput after the response headers. */
88
+ export async function measureDownloadSpeed(url, options = {}) {
89
+ const maxBytes = options.maxBytes ?? 32 * 1024 * 1024;
90
+ const maxMs = options.maxMs ?? 5_000;
91
+ const controller = new AbortController();
92
+ const headerTimer = setTimeout(() => controller.abort(), options.headerTimeoutMs ?? 10_000);
93
+ let response;
94
+ try {
95
+ response = await fetch(url, {
96
+ headers: { 'user-agent': USER_AGENT, range: `bytes=0-${maxBytes - 1}` },
97
+ redirect: 'follow',
98
+ signal: controller.signal,
99
+ });
100
+ }
101
+ catch (error) {
102
+ throw new Error(`Could not reach ${new URL(url).host}: ${toError(error).message}`);
103
+ }
104
+ finally {
105
+ clearTimeout(headerTimer);
106
+ }
107
+ if (!response.ok || !response.body)
108
+ throw new HttpError(response.status, '');
109
+ const reader = response.body.getReader();
110
+ const started = performance.now();
111
+ const deadline = setTimeout(() => { void reader.cancel().catch(() => undefined); }, maxMs);
112
+ let bytes = 0;
113
+ try {
114
+ while (bytes < maxBytes) {
115
+ const { done, value } = await reader.read();
116
+ if (done)
117
+ break;
118
+ bytes += value.byteLength;
119
+ }
120
+ }
121
+ finally {
122
+ clearTimeout(deadline);
123
+ await reader.cancel().catch(() => undefined);
124
+ }
125
+ const ms = Math.max(1, performance.now() - started);
126
+ return { bytes, ms, bytesPerSec: bytes / (ms / 1000) };
127
+ }
128
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,3 @@
1
+ export declare function instanceLockPath(env?: NodeJS.ProcessEnv): string;
2
+ /** Exclusive ownership of SUBLET_HOME. A lock left by a process that is gone is taken over; any other lock is refused. */
3
+ export declare function acquireInstanceLock(env?: NodeJS.ProcessEnv): Promise<() => Promise<void>>;
@@ -0,0 +1,108 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, open, readFile, unlink } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { subletHome } from '../config.js';
5
+ import { ensureDir } from './fs.js';
6
+ export function instanceLockPath(env = process.env) {
7
+ return join(subletHome(env), 'agent.lock');
8
+ }
9
+ /** Creates the file only if it does not exist yet; null when it already does. */
10
+ async function createExclusive(path) {
11
+ try {
12
+ return await open(path, 'wx', 0o600);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'EEXIST')
16
+ return null;
17
+ throw error;
18
+ }
19
+ }
20
+ async function readOwner(path) {
21
+ try {
22
+ const owner = JSON.parse(await readFile(path, 'utf8'));
23
+ return owner && typeof owner === 'object' ? owner : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /** True unless the PID is certainly gone. An unknown PID counts as alive, so an unreadable lock fails closed. EPERM means the process exists but belongs to another user. */
30
+ function processAlive(pid) {
31
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
32
+ return true;
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch (error) {
38
+ return error.code !== 'ESRCH';
39
+ }
40
+ }
41
+ function lockedError(path, detail) {
42
+ return new Error(`SUBLET_HOME is locked: ${path}. ${detail} Do not delete state.json.`);
43
+ }
44
+ /**
45
+ * Removes a lock whose process is gone, after a crash, so an agent restarted by a service manager restores its
46
+ * booking. A live or unknown owner keeps the lock (PID reuse only ever makes it look alive, which fails closed).
47
+ * `agent.lock.reclaim` lets one process at a time check and remove a stale lock, so two reclaimers cannot both
48
+ * remove it, and a fresh lock (another id) is never removed. Ownership is still decided only by the exclusive create.
49
+ */
50
+ async function reclaimStale(path) {
51
+ const owner = await readOwner(path);
52
+ if (!owner || typeof owner.id !== 'string')
53
+ throw lockedError(path, 'Its owner cannot be read. If no agent or maintenance task uses this directory, remove only agent.lock.');
54
+ if (processAlive(owner.pid))
55
+ throw lockedError(path, `Another agent or maintenance task is running (process ${String(owner.pid)}).`);
56
+ const guardPath = `${path}.reclaim`;
57
+ const guard = await createExclusive(guardPath);
58
+ if (!guard)
59
+ throw lockedError(path, `Another process is taking over a stale lock. If none is, remove ${guardPath}.`);
60
+ await guard.close();
61
+ try {
62
+ if ((await readOwner(path))?.id === owner.id) {
63
+ await unlink(path).catch((error) => {
64
+ if (error.code !== 'ENOENT')
65
+ throw error;
66
+ });
67
+ }
68
+ }
69
+ finally {
70
+ await unlink(guardPath).catch(() => undefined);
71
+ }
72
+ }
73
+ /** Exclusive ownership of SUBLET_HOME. A lock left by a process that is gone is taken over; any other lock is refused. */
74
+ export async function acquireInstanceLock(env = process.env) {
75
+ await ensureDir(subletHome(env));
76
+ const path = instanceLockPath(env);
77
+ const id = randomUUID();
78
+ let handle = await createExclusive(path);
79
+ if (!handle) {
80
+ await reclaimStale(path);
81
+ handle = await createExclusive(path);
82
+ if (!handle)
83
+ throw lockedError(path, 'Another agent or maintenance task started at the same time.');
84
+ }
85
+ try {
86
+ await handle.writeFile(JSON.stringify({ version: 1, pid: process.pid, id, createdAt: Date.now() }));
87
+ await handle.sync();
88
+ }
89
+ catch (error) {
90
+ await handle.close();
91
+ await unlink(path).catch(() => undefined);
92
+ throw error;
93
+ }
94
+ await handle.close();
95
+ let releasePromise;
96
+ // Publish one promise synchronously: overlapping calls must not both unlink,
97
+ // and neither successful nor failed releases may later touch a new owner's lock.
98
+ return () => releasePromise ??= (async () => {
99
+ const info = await lstat(path);
100
+ if (!info.isFile() || info.isSymbolicLink())
101
+ throw new Error('Instance lock changed; refusing to remove it');
102
+ const owner = JSON.parse(await readFile(path, 'utf8'));
103
+ if (owner.id !== id)
104
+ throw new Error('Instance lock ownership changed; refusing to remove it');
105
+ await unlink(path);
106
+ })();
107
+ }
108
+ //# sourceMappingURL=instanceLock.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The link to hand to the browser, or null when it is not a plain web link. A link that came from the server is
3
+ * opened only on the server's own origin (`sameOriginAs`), so a server cannot make this machine open a program,
4
+ * a file share or another site. Without `sameOriginAs`, only https and this machine's http are allowed.
5
+ */
6
+ export declare function browserUrl(raw: string, sameOriginAs?: string): string | null;
7
+ /**
8
+ * Opens a web link in the default browser. Returns false when the link is refused or no browser could start.
9
+ * On Windows it goes through rundll32 by its full path, not `cmd /c start`, so characters like `&` in the link are
10
+ * never read as commands and a program named `cmd` in the current folder is never run.
11
+ */
12
+ export declare function openUrl(raw: string, sameOriginAs?: string): boolean;
13
+ /** Text from the server, safe to print: control characters (terminal escape sequences) are removed. */
14
+ export declare function printable(text: string): string;
15
+ /** A browser on this machine helps only in a local desktop session, not over SSH or on a headless server. */
16
+ export declare function canOpenBrowser(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { isLoopbackHost } from '../config.js';
4
+ /**
5
+ * The link to hand to the browser, or null when it is not a plain web link. A link that came from the server is
6
+ * opened only on the server's own origin (`sameOriginAs`), so a server cannot make this machine open a program,
7
+ * a file share or another site. Without `sameOriginAs`, only https and this machine's http are allowed.
8
+ */
9
+ export function browserUrl(raw, sameOriginAs) {
10
+ let url;
11
+ try {
12
+ url = new URL(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
18
+ return null;
19
+ if (url.username || url.password)
20
+ return null;
21
+ if (sameOriginAs !== undefined) {
22
+ try {
23
+ return new URL(sameOriginAs).origin === url.origin ? url.href : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ return url.protocol === 'https:' || isLoopbackHost(url.hostname) ? url.href : null;
30
+ }
31
+ /**
32
+ * Opens a web link in the default browser. Returns false when the link is refused or no browser could start.
33
+ * On Windows it goes through rundll32 by its full path, not `cmd /c start`, so characters like `&` in the link are
34
+ * never read as commands and a program named `cmd` in the current folder is never run.
35
+ */
36
+ export function openUrl(raw, sameOriginAs) {
37
+ const url = browserUrl(raw, sameOriginAs);
38
+ if (!url)
39
+ return false;
40
+ try {
41
+ const child = process.platform === 'win32'
42
+ ? spawn(join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'rundll32.exe'), ['url.dll,FileProtocolHandler', url], { detached: true, stdio: 'ignore', windowsHide: true })
43
+ : spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' });
44
+ // A missing opener (no xdg-open) is reported as an 'error' event, which would crash the process without a listener.
45
+ child.on('error', () => undefined);
46
+ child.unref();
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /** Text from the server, safe to print: control characters (terminal escape sequences) are removed. */
54
+ export function printable(text) {
55
+ return text.replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
56
+ }
57
+ /** A browser on this machine helps only in a local desktop session, not over SSH or on a headless server. */
58
+ export function canOpenBrowser(env = process.env, platform = process.platform) {
59
+ if (env.SSH_CONNECTION || env.SSH_TTY)
60
+ return false;
61
+ if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY)
62
+ return false;
63
+ return true;
64
+ }
65
+ //# sourceMappingURL=open.js.map
@@ -0,0 +1,10 @@
1
+ export declare function storagePolicy(env?: NodeJS.ProcessEnv): {
2
+ modelCacheBytes: number;
3
+ reserveBytes: number;
4
+ };
5
+ /** Count all regular files, including partial downloads. Never follow directory links. */
6
+ export declare function directoryBytes(root: string): Promise<number>;
7
+ export declare function availableWriteBytes(free: number, reserve: number): number;
8
+ export declare function diskWriteAllowance(path: string, reserve: number): Promise<number>;
9
+ export declare function storageLimitError(): Error;
10
+ export declare function requireDiskSpace(path: string, bytes: number, reserve: number): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { freeDiskBytes } from './fs.js';
4
+ import { formatBytes } from './format.js';
5
+ import { brandEnv } from './env.js';
6
+ const GIB = 1024 ** 3;
7
+ function gibSetting(env, key, fallback) {
8
+ const raw = brandEnv(env, key);
9
+ const value = raw == null ? fallback : Number(raw);
10
+ if ((raw != null && !raw.trim()) || !Number.isFinite(value) || value <= 0 || value > 1024 * 1024) {
11
+ throw new Error(`${key} must be a positive number of GiB (at most 1048576)`);
12
+ }
13
+ const bytes = Math.floor(value * GIB);
14
+ if (bytes < 1)
15
+ throw new Error(`${key} must represent at least one byte`);
16
+ return bytes;
17
+ }
18
+ export function storagePolicy(env = process.env) {
19
+ return {
20
+ modelCacheBytes: gibSetting(env, 'SUBLET_MODEL_CACHE_GB', 20),
21
+ reserveBytes: gibSetting(env, 'SUBLET_MIN_FREE_DISK_GB', 5),
22
+ };
23
+ }
24
+ /** Count all regular files, including partial downloads. Never follow directory links. */
25
+ export async function directoryBytes(root) {
26
+ let bytes = 0;
27
+ const pending = [root];
28
+ while (pending.length) {
29
+ const path = pending.pop();
30
+ let info;
31
+ try {
32
+ info = await lstat(path);
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'ENOENT')
36
+ continue;
37
+ throw error;
38
+ }
39
+ if (info.isSymbolicLink())
40
+ throw new Error(`Storage accounting refuses symbolic links: ${path}`);
41
+ if (info.isFile())
42
+ bytes += info.size;
43
+ else if (info.isDirectory()) {
44
+ for (const name of await readdir(path))
45
+ pending.push(join(path, name));
46
+ }
47
+ else
48
+ throw new Error(`Storage accounting requires regular files: ${path}`);
49
+ }
50
+ return bytes;
51
+ }
52
+ export function availableWriteBytes(free, reserve) {
53
+ return Math.max(0, free - reserve);
54
+ }
55
+ export async function diskWriteAllowance(path, reserve) {
56
+ const free = await freeDiskBytes(path);
57
+ if (free == null)
58
+ throw new Error('Cannot determine free disk space; refusing an unbounded download');
59
+ return availableWriteBytes(free, reserve);
60
+ }
61
+ export function storageLimitError() {
62
+ return new Error('Download stopped by the storage budget. Free disk space or move SUBLET_HOME to a larger drive. Adjust SUBLET_MODEL_CACHE_GB / SUBLET_MIN_FREE_DISK_GB if appropriate; no existing models were automatically deleted.');
63
+ }
64
+ export async function requireDiskSpace(path, bytes, reserve) {
65
+ const allowance = await diskWriteAllowance(path, reserve);
66
+ if (bytes > allowance)
67
+ throw new Error(`Not enough disk space: ${formatBytes(bytes)} needed while retaining ${formatBytes(reserve)} free. ${storageLimitError().message}`);
68
+ }
69
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1,15 @@
1
+ export declare const BRAND = "Sublera";
2
+ export declare const VERSION = "1.3.0";
3
+ /** Lowest supported Node.js; matches "engines" in package.json (undici, used for proxies, needs 20.18.1). */
4
+ export declare const MIN_NODE = "20.18.1";
5
+ export declare const AGENT_NAME = "sublera-cli";
6
+ export declare const USER_AGENT = "sublera-cli/1.3.0";
7
+ export declare const DEFAULT_SERVER: string;
8
+ export declare const DEFAULT_WS_PATH: string;
9
+ export declare const HEARTBEAT_MS = 5000;
10
+ export declare const RECONNECT_DELAYS_MS: number[];
11
+ /** WebSocket close code a server sends to refuse an agent that is too old (mirrors HTTP 426). */
12
+ export declare const UPGRADE_REQUIRED_CLOSE = 4426;
13
+ /** Compares dotted numeric versions. Pre-release tags are ignored. */
14
+ export declare function compareVersions(a: string, b: string): number;
15
+ export declare function outdatedMessage(minVersion?: string | null): string;
@@ -0,0 +1,29 @@
1
+ import { brandEnv } from './util/env.js';
2
+ export const BRAND = 'Sublera';
3
+ export const VERSION = '1.3.0';
4
+ /** Lowest supported Node.js; matches "engines" in package.json (undici, used for proxies, needs 20.18.1). */
5
+ export const MIN_NODE = '20.18.1';
6
+ export const AGENT_NAME = 'sublera-cli';
7
+ export const USER_AGENT = `${AGENT_NAME}/${VERSION}`;
8
+ export const DEFAULT_SERVER = brandEnv(process.env, 'SUBLET_SERVER') || 'https://sublera.com';
9
+ export const DEFAULT_WS_PATH = brandEnv(process.env, 'SUBLET_WS_PATH') || '/tunnel';
10
+ export const HEARTBEAT_MS = 5_000;
11
+ export const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 15_000, 30_000];
12
+ /** WebSocket close code a server sends to refuse an agent that is too old (mirrors HTTP 426). */
13
+ export const UPGRADE_REQUIRED_CLOSE = 4426;
14
+ /** Compares dotted numeric versions. Pre-release tags are ignored. */
15
+ export function compareVersions(a, b) {
16
+ const parts = (v) => v.replace(/^v/, '').split(/[.+-]/).slice(0, 3).map((n) => Number.parseInt(n, 10) || 0);
17
+ const [pa, pb] = [parts(a), parts(b)];
18
+ for (let i = 0; i < 3; i++) {
19
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
20
+ if (diff)
21
+ return Math.sign(diff);
22
+ }
23
+ return 0;
24
+ }
25
+ export function outdatedMessage(minVersion) {
26
+ const needs = minVersion ? ` (needs v${minVersion} or newer)` : '';
27
+ return `${AGENT_NAME} v${VERSION} is too old for this server${needs}. Update with: npx ${AGENT_NAME}@latest`;
28
+ }
29
+ //# sourceMappingURL=version.js.map