@lasso-ai/cli 1.0.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.
- package/ARCHITECTURE.md +342 -0
- package/CHANGELOG.md +102 -0
- package/CODE_OF_CONDUCT.md +129 -0
- package/CONTRIBUTING.md +122 -0
- package/LICENSE +7 -0
- package/README.md +548 -0
- package/SECURITY.md +65 -0
- package/SUPPORT.md +31 -0
- package/dist/cli/agent.d.ts +61 -0
- package/dist/cli/agent.js +418 -0
- package/dist/cli/auth.d.ts +38 -0
- package/dist/cli/auth.js +162 -0
- package/dist/cli/bridge.d.ts +160 -0
- package/dist/cli/bridge.js +450 -0
- package/dist/cli/host/certificates.d.ts +12 -0
- package/dist/cli/host/certificates.js +52 -0
- package/dist/cli/host/client.d.ts +49 -0
- package/dist/cli/host/client.js +141 -0
- package/dist/cli/host/daemon.d.ts +50 -0
- package/dist/cli/host/daemon.js +368 -0
- package/dist/cli/host/dns.d.ts +80 -0
- package/dist/cli/host/dns.js +255 -0
- package/dist/cli/host/install.d.ts +32 -0
- package/dist/cli/host/install.js +177 -0
- package/dist/cli/host/next-host-entry.d.ts +1 -0
- package/dist/cli/host/next-host-entry.js +82 -0
- package/dist/cli/host/paths.d.ts +15 -0
- package/dist/cli/host/paths.js +55 -0
- package/dist/cli/host/registry.d.ts +31 -0
- package/dist/cli/host/registry.js +103 -0
- package/dist/cli/host/runtime.d.ts +23 -0
- package/dist/cli/host/runtime.js +134 -0
- package/dist/cli/host/vite-host-entry.d.ts +1 -0
- package/dist/cli/host/vite-host-entry.js +59 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +322 -0
- package/dist/cli/project.d.ts +56 -0
- package/dist/cli/project.js +298 -0
- package/dist/cli/server/next.d.ts +6 -0
- package/dist/cli/server/next.js +76 -0
- package/dist/cli/server/vite.d.ts +1 -0
- package/dist/cli/server/vite.js +51 -0
- package/dist/cli/utils/framework.d.ts +2 -0
- package/dist/cli/utils/framework.js +26 -0
- package/dist/overlay.js +20365 -0
- package/docs/PUBLISHING.md +63 -0
- package/package.json +106 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WindowsResolver = exports.LinuxResolver = exports.MacOSResolver = void 0;
|
|
7
|
+
exports.buildDnsResponse = buildDnsResponse;
|
|
8
|
+
exports.startDnsServer = startDnsServer;
|
|
9
|
+
exports.createDomainResolver = createDomainResolver;
|
|
10
|
+
exports.syncResolverAfterRegister = syncResolverAfterRegister;
|
|
11
|
+
const node_dgram_1 = __importDefault(require("node:dgram"));
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_child_process_1 = require("node:child_process");
|
|
15
|
+
const node_util_1 = require("node:util");
|
|
16
|
+
const paths_1 = require("./paths");
|
|
17
|
+
const registry_1 = require("./registry");
|
|
18
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
19
|
+
const LOOPBACK = "127.0.0.1";
|
|
20
|
+
function sudoWrite(file, content) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = (0, node_child_process_1.spawn)("sudo", ["tee", file], { stdio: ["pipe", "ignore", "inherit"] });
|
|
23
|
+
child.on("error", reject);
|
|
24
|
+
child.on("close", (code) => {
|
|
25
|
+
if (code === 0)
|
|
26
|
+
resolve();
|
|
27
|
+
else
|
|
28
|
+
reject(new Error(`sudo tee exited with code ${code}`));
|
|
29
|
+
});
|
|
30
|
+
child.stdin.end(content);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ---- Minimal DNS responder — answers `*.lasso` → 127.0.0.1 so the local
|
|
34
|
+
// domain namespace needs no per-project /etc/hosts entries.
|
|
35
|
+
function encodeName(name) {
|
|
36
|
+
const parts = name.replace(/\.$/, "").split(".");
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
chunks.push(part.length);
|
|
40
|
+
for (const byte of Buffer.from(part, "ascii"))
|
|
41
|
+
chunks.push(byte);
|
|
42
|
+
}
|
|
43
|
+
chunks.push(0);
|
|
44
|
+
return Buffer.from(chunks);
|
|
45
|
+
}
|
|
46
|
+
function parseName(buf, offset) {
|
|
47
|
+
const labels = [];
|
|
48
|
+
let i = offset;
|
|
49
|
+
while (true) {
|
|
50
|
+
const len = buf[i];
|
|
51
|
+
if (len === 0) {
|
|
52
|
+
i += 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if ((len & 0xc0) === 0xc0) {
|
|
56
|
+
i += 2;
|
|
57
|
+
return { name: labels.join(".") === "." ? "" : labels.join("."), next: i };
|
|
58
|
+
}
|
|
59
|
+
labels.push(buf.subarray(i + 1, i + 1 + len).toString("ascii"));
|
|
60
|
+
i += 1 + len;
|
|
61
|
+
}
|
|
62
|
+
return { name: labels.join("."), next: i };
|
|
63
|
+
}
|
|
64
|
+
function buildDnsResponse(query) {
|
|
65
|
+
if (query.length < 12)
|
|
66
|
+
return null;
|
|
67
|
+
const id = query.subarray(0, 2);
|
|
68
|
+
const qdcount = query.readUInt16BE(4);
|
|
69
|
+
if (qdcount === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const { name, next } = parseName(query, 12);
|
|
72
|
+
const qtype = query.readUInt16BE(next);
|
|
73
|
+
const raiseRecursion = query[2] & 0x01; // RD flag is byte 2 bit 0
|
|
74
|
+
const isLasso = name.endsWith(`.${paths_1.TLD}`);
|
|
75
|
+
const rcode = isLasso ? 0 : 3; // NXDOMAIN for anything outside the namespace
|
|
76
|
+
const flags = 0x8000 | (raiseRecursion ? 0x0100 : 0) | 0x0400 | (rcode & 0x0f); // QR|RD|RA|rcode
|
|
77
|
+
const ancount = isLasso && qtype === 1 ? 1 : 0; // one A record when asked for A
|
|
78
|
+
const header = Buffer.alloc(12);
|
|
79
|
+
id.copy(header, 0);
|
|
80
|
+
header.writeUInt16BE(flags, 2);
|
|
81
|
+
header.writeUInt16BE(qdcount, 4);
|
|
82
|
+
header.writeUInt16BE(ancount, 6);
|
|
83
|
+
header.writeUInt16BE(0, 8);
|
|
84
|
+
header.writeUInt16BE(0, 10);
|
|
85
|
+
if (ancount === 0) {
|
|
86
|
+
return Buffer.concat([header, query.subarray(12, next + 4)]);
|
|
87
|
+
}
|
|
88
|
+
const answer = Buffer.alloc(16);
|
|
89
|
+
answer.writeUInt16BE(0xc00c, 0); // pointer to the question name
|
|
90
|
+
answer.writeUInt16BE(1, 2); // Type A
|
|
91
|
+
answer.writeUInt16BE(1, 4); // Class IN
|
|
92
|
+
answer.writeUInt32BE(300, 6); // TTL
|
|
93
|
+
answer.writeUInt16BE(4, 10); // RDLENGTH
|
|
94
|
+
for (const [i, octet] of LOOPBACK.split(".").map(Number).entries())
|
|
95
|
+
answer[12 + i] = octet;
|
|
96
|
+
return Buffer.concat([header, query.subarray(12, next + 4), answer]);
|
|
97
|
+
}
|
|
98
|
+
function startDnsServer(port) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const socket = node_dgram_1.default.createSocket("udp4");
|
|
101
|
+
socket.on("message", (message, remote) => {
|
|
102
|
+
const reply = buildDnsResponse(message);
|
|
103
|
+
if (reply)
|
|
104
|
+
socket.send(reply, remote.port, remote.address);
|
|
105
|
+
});
|
|
106
|
+
socket.on("error", (error) => reject(error));
|
|
107
|
+
socket.bind(port, LOOPBACK, () => {
|
|
108
|
+
const address = socket.address();
|
|
109
|
+
resolve({ close: () => socket.close(), port: address.port });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const MAC_RESOLVER_FILE = "/etc/resolver/lasso";
|
|
114
|
+
function macResolverContent(dnsPort) {
|
|
115
|
+
return `nameserver ${LOOPBACK}\nport ${dnsPort}\ntimeout 1\n`;
|
|
116
|
+
}
|
|
117
|
+
class MacOSResolver {
|
|
118
|
+
platform = "darwin";
|
|
119
|
+
async status() {
|
|
120
|
+
try {
|
|
121
|
+
const content = node_fs_1.default.readFileSync(MAC_RESOLVER_FILE, "utf8");
|
|
122
|
+
const port = content.match(/^port\s+(\d+)/m)?.[1] || "unknown";
|
|
123
|
+
return { installed: content.includes("nameserver"), detail: `/etc/resolver/lasso → 127.0.0.1:${port}` };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { installed: false, detail: "`lasso` domain not configured (`/etc/resolver/lasso` missing)" };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async install(dnsPort) {
|
|
130
|
+
try {
|
|
131
|
+
// Writing to /etc/resolver needs the admin password (macOS maps it from
|
|
132
|
+
// the foreground terminal when sudo prompts).
|
|
133
|
+
// The resolver directory is not present on every macOS installation.
|
|
134
|
+
// Create it first so the subsequent sudo tee does not fail with ENOENT.
|
|
135
|
+
await execFileAsync("sudo", ["mkdir", "-p", node_path_1.default.dirname(MAC_RESOLVER_FILE)]);
|
|
136
|
+
await sudoWrite(MAC_RESOLVER_FILE, macResolverContent(dnsPort));
|
|
137
|
+
return { ok: true, message: `Installed /etc/resolver/lasso → 127.0.0.1:${dnsPort}` };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
141
|
+
return { ok: false, requiresSudo: true, message: `Could not write /etc/resolver/lasso (${reason}). Run "lasso daemon install" from a terminal so you can enter your password.` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async uninstall() {
|
|
145
|
+
try {
|
|
146
|
+
await execFileAsync("sudo", ["rm", "-f", MAC_RESOLVER_FILE]);
|
|
147
|
+
return { ok: true, message: "Removed /etc/resolver/lasso." };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
151
|
+
return { ok: false, message: `Could not remove /etc/resolver/lasso (${reason}).` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.MacOSResolver = MacOSResolver;
|
|
156
|
+
class LinuxResolver {
|
|
157
|
+
platform = "linux";
|
|
158
|
+
systemdFile = "/etc/systemd/resolved.conf.d/lasso-host.conf";
|
|
159
|
+
async status() {
|
|
160
|
+
return { installed: node_fs_1.default.existsSync(this.systemdFile), detail: node_fs_1.default.existsSync(this.systemdFile) ? `systemd-resolved split-DNS configured for ~.${paths_1.TLD}` : "`lasso` domain not configured (systemd-resolved split-DNS missing)" };
|
|
161
|
+
}
|
|
162
|
+
async install(dnsPort) {
|
|
163
|
+
const body = `[Resolve]\nDNS=${LOOPBACK}#${dnsPort}\nDomains=~${paths_1.TLD}\n`;
|
|
164
|
+
try {
|
|
165
|
+
await execFileAsync("sudo", ["mkdir", "-p", node_path_1.default.dirname(this.systemdFile)]);
|
|
166
|
+
await sudoWrite(this.systemdFile, body);
|
|
167
|
+
await execFileAsync("sudo", ["systemctl", "try-restart", "systemd-resolved"]).catch(() => undefined);
|
|
168
|
+
return { ok: true, requiresSudo: true, message: `Installed systemd-resolved split-DNS for *.${paths_1.TLD} → 127.0.0.1:${dnsPort}` };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
172
|
+
return { ok: false, requiresSudo: true, message: `Could not configure systemd-resolved (${reason}).` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async uninstall() {
|
|
176
|
+
try {
|
|
177
|
+
await execFileAsync("sudo", ["rm", "-f", this.systemdFile]);
|
|
178
|
+
await execFileAsync("sudo", ["systemctl", "try-restart", "systemd-resolved"]).catch(() => undefined);
|
|
179
|
+
return { ok: true, message: "Removed systemd-resolved split-DNS configuration." };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
183
|
+
return { ok: false, message: `Could not remove systemd-resolved configuration (${reason}).` };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.LinuxResolver = LinuxResolver;
|
|
188
|
+
const WINDOWS_HOSTS = node_path_1.default.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
|
|
189
|
+
const HOSTS_BLOCK_START = "# --- lasso-host ---";
|
|
190
|
+
const HOSTS_BLOCK_END = "# --- end lasso-host ---";
|
|
191
|
+
class WindowsResolver {
|
|
192
|
+
platform = "win32";
|
|
193
|
+
async status() {
|
|
194
|
+
const synced = await this.readHostsBlock();
|
|
195
|
+
return { installed: synced.length > 0, detail: synced.length > 0 ? `${synced.length} registered .${paths_1.TLD} domain(s) in hosts` : "no .lasso domains registered in hosts" };
|
|
196
|
+
}
|
|
197
|
+
async readHostsBlock() {
|
|
198
|
+
try {
|
|
199
|
+
const content = node_fs_1.default.readFileSync(WINDOWS_HOSTS, "utf8");
|
|
200
|
+
const match = content.match(new RegExp(`${HOSTS_BLOCK_START}\\n([\\s\\S]*?)\\n${HOSTS_BLOCK_END}`));
|
|
201
|
+
if (!match)
|
|
202
|
+
return [];
|
|
203
|
+
return match[1].split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
decodeHost(entry) {
|
|
210
|
+
return entry.split(/\s+/)[1] || "";
|
|
211
|
+
}
|
|
212
|
+
async sync(registry) {
|
|
213
|
+
const lines = [`${HOSTS_BLOCK_START}`];
|
|
214
|
+
for (const domain of Object.keys(registry))
|
|
215
|
+
lines.push(`${LOOPBACK} ${domain}`);
|
|
216
|
+
lines.push(`${HOSTS_BLOCK_END}`);
|
|
217
|
+
const block = lines.join("\n");
|
|
218
|
+
const contents = node_fs_1.default.existsSync(WINDOWS_HOSTS) ? node_fs_1.default.readFileSync(WINDOWS_HOSTS, "utf8") : "";
|
|
219
|
+
const next = contents.includes(HOSTS_BLOCK_START) ? contents.replace(new RegExp(`${HOSTS_BLOCK_START}\\n[\\s\\S]*?\\n${HOSTS_BLOCK_END}`), block) : contents.trimEnd() + "\n" + block + "\n";
|
|
220
|
+
try {
|
|
221
|
+
await execFileAsync("net", ["session"]).catch(() => undefined);
|
|
222
|
+
node_fs_1.default.writeFileSync(WINDOWS_HOSTS, next);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async install() {
|
|
230
|
+
return { ok: false, message: "Windows has no per-domain DNS configuration. `lasso register` will add each registered domain to the hosts file instead (run from an elevated terminal)." };
|
|
231
|
+
}
|
|
232
|
+
async uninstall() {
|
|
233
|
+
return { ok: true, message: "Remove the '# --- lasso-host ---' block from your hosts file to remove Lasso Host mappings." };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
exports.WindowsResolver = WindowsResolver;
|
|
237
|
+
let resolverSingleton = null;
|
|
238
|
+
function createDomainResolver(platform = process.platform) {
|
|
239
|
+
if (!resolverSingleton) {
|
|
240
|
+
if (platform === "darwin")
|
|
241
|
+
resolverSingleton = new MacOSResolver();
|
|
242
|
+
else if (platform === "linux")
|
|
243
|
+
resolverSingleton = new LinuxResolver();
|
|
244
|
+
else
|
|
245
|
+
resolverSingleton = new WindowsResolver();
|
|
246
|
+
}
|
|
247
|
+
return resolverSingleton;
|
|
248
|
+
}
|
|
249
|
+
// Windows keeps hosts entries in sync whenever the registry changes.
|
|
250
|
+
function syncResolverAfterRegister() {
|
|
251
|
+
if (process.platform !== "win32")
|
|
252
|
+
return Promise.resolve(true);
|
|
253
|
+
const resolver = createDomainResolver();
|
|
254
|
+
return resolver.sync((0, registry_1.loadRegistry)());
|
|
255
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { hostAlive } from "./client";
|
|
2
|
+
import { type DomainResolver } from "./dns";
|
|
3
|
+
export declare function spawnDaemon(env: Record<string, string>): Promise<{
|
|
4
|
+
ok: boolean;
|
|
5
|
+
alreadyRunning?: boolean;
|
|
6
|
+
pid?: number;
|
|
7
|
+
error?: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function isAutoStartInstalled(): boolean;
|
|
10
|
+
export declare function enableAutoStart(): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function disableAutoStart(): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
}>;
|
|
18
|
+
export interface DaemonStatusReport {
|
|
19
|
+
running: boolean;
|
|
20
|
+
pid?: number | null;
|
|
21
|
+
health: Awaited<ReturnType<typeof hostAlive>>;
|
|
22
|
+
autoStart: boolean;
|
|
23
|
+
resolver: Awaited<ReturnType<DomainResolver["status"]>>;
|
|
24
|
+
}
|
|
25
|
+
export declare function daemonStatus(env: Record<string, string>): Promise<DaemonStatusReport>;
|
|
26
|
+
export declare function stopDaemon(env: Record<string, string>): Promise<{
|
|
27
|
+
ok: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function daemonPortMessage(env: Record<string, string>): string;
|
|
31
|
+
export declare function daemonHttpsPortMessage(env: Record<string, string>): string;
|
|
32
|
+
export declare function yesOrNo(promptText: string): Promise<boolean>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.spawnDaemon = spawnDaemon;
|
|
7
|
+
exports.isAutoStartInstalled = isAutoStartInstalled;
|
|
8
|
+
exports.enableAutoStart = enableAutoStart;
|
|
9
|
+
exports.disableAutoStart = disableAutoStart;
|
|
10
|
+
exports.daemonStatus = daemonStatus;
|
|
11
|
+
exports.stopDaemon = stopDaemon;
|
|
12
|
+
exports.daemonPortMessage = daemonPortMessage;
|
|
13
|
+
exports.daemonHttpsPortMessage = daemonHttpsPortMessage;
|
|
14
|
+
exports.yesOrNo = yesOrNo;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
19
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
|
+
const client_1 = require("./client");
|
|
21
|
+
const dns_1 = require("./dns");
|
|
22
|
+
const paths_1 = require("./paths");
|
|
23
|
+
const AGENT_LABEL = "com.lasso.host";
|
|
24
|
+
const LAUNCH_AGENT_FILE = node_path_1.default.join(node_os_1.default.homedir(), "Library", "LaunchAgents", `${AGENT_LABEL}.plist`);
|
|
25
|
+
function cliEntry() {
|
|
26
|
+
const entry = process.argv[1];
|
|
27
|
+
return entry && entry.endsWith(".js") ? entry : null;
|
|
28
|
+
}
|
|
29
|
+
function spawnDaemon(env) {
|
|
30
|
+
const port = (0, paths_1.hostProxyPort)(env);
|
|
31
|
+
const dnsPort = (0, paths_1.hostDnsPort)(env);
|
|
32
|
+
const httpsPort = (0, paths_1.hostHttpsPort)(env);
|
|
33
|
+
return (0, client_1.hostAlive)(port).then(async (health) => {
|
|
34
|
+
if (health)
|
|
35
|
+
return { ok: true, alreadyRunning: true };
|
|
36
|
+
const entry = cliEntry();
|
|
37
|
+
if (!entry)
|
|
38
|
+
return { ok: false, error: "Could not resolve the Lasso CLI entry point. Run this from the installed CLI." };
|
|
39
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [entry, "_host"], {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
cwd: node_os_1.default.homedir(),
|
|
43
|
+
env: { ...process.env, LASSO_HOST_PORT: String(port), LASSO_HOST_HTTPS_PORT: String(httpsPort), LASSO_DNS_PORT: String(dnsPort) },
|
|
44
|
+
});
|
|
45
|
+
const pid = child.pid;
|
|
46
|
+
child.unref();
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 1800));
|
|
48
|
+
if (await (0, client_1.hostAlive)(port))
|
|
49
|
+
return { ok: true, pid };
|
|
50
|
+
return { ok: false, error: `Lasso Host did not come up on port ${port}. Check ${paths_1.LOG_FILE}.` };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function launchAgentPlist(entry) {
|
|
54
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
55
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
56
|
+
<plist version="1.0">
|
|
57
|
+
<dict>
|
|
58
|
+
<key>Label</key>
|
|
59
|
+
<string>${AGENT_LABEL}</string>
|
|
60
|
+
<key>ProgramArguments</key>
|
|
61
|
+
<array>
|
|
62
|
+
<string>${process.execPath}</string>
|
|
63
|
+
<string>${entry}</string>
|
|
64
|
+
<string>_host</string>
|
|
65
|
+
</array>
|
|
66
|
+
<key>RunAtLoad</key>
|
|
67
|
+
<true/>
|
|
68
|
+
<key>KeepAlive</key>
|
|
69
|
+
<false/>
|
|
70
|
+
<key>StandardOutPath</key>
|
|
71
|
+
<string>${paths_1.LOG_FILE}</string>
|
|
72
|
+
<key>StandardErrorPath</key>
|
|
73
|
+
<string>${paths_1.LOG_FILE}</string>
|
|
74
|
+
</dict>
|
|
75
|
+
</plist>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function isAutoStartInstalled() {
|
|
79
|
+
if (process.platform !== "darwin")
|
|
80
|
+
return false;
|
|
81
|
+
return node_fs_1.default.existsSync(LAUNCH_AGENT_FILE);
|
|
82
|
+
}
|
|
83
|
+
function launchctl(args) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const child = (0, node_child_process_1.spawn)("launchctl", args);
|
|
86
|
+
let out = "";
|
|
87
|
+
child.stdout.on("data", (d) => (out += d));
|
|
88
|
+
child.stderr.on("data", (d) => (out += d));
|
|
89
|
+
child.on("close", (code) => resolve(out.trim()));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const uid = node_os_1.default.userInfo().uid;
|
|
93
|
+
async function enableAutoStart() {
|
|
94
|
+
const entry = cliEntry();
|
|
95
|
+
if (!entry)
|
|
96
|
+
return { ok: false, error: "Could not resolve the Lasso CLI entry point. Run this from the installed CLI." };
|
|
97
|
+
if (process.platform !== "darwin") {
|
|
98
|
+
return { ok: true, error: undefined };
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(LAUNCH_AGENT_FILE), { recursive: true });
|
|
102
|
+
node_fs_1.default.writeFileSync(LAUNCH_AGENT_FILE, launchAgentPlist(entry), { mode: 0o644 });
|
|
103
|
+
await launchctl(["bootstrap", `gui/${uid}`, LAUNCH_AGENT_FILE]);
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
108
|
+
return { ok: false, error: `Could not install the launch agent (${reason}).` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function disableAutoStart() {
|
|
112
|
+
if (process.platform !== "darwin")
|
|
113
|
+
return { ok: true };
|
|
114
|
+
try {
|
|
115
|
+
await launchctl(["bootout", `gui/${uid}/${AGENT_LABEL}`]);
|
|
116
|
+
node_fs_1.default.rmSync(LAUNCH_AGENT_FILE, { force: true });
|
|
117
|
+
return { ok: true };
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const reason = error instanceof Error ? error.message : "failed";
|
|
121
|
+
return { ok: false, error: `Could not remove the launch agent (${reason}).` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function daemonStatus(env) {
|
|
125
|
+
const port = (0, paths_1.hostProxyPort)(env);
|
|
126
|
+
const pid = (0, client_1.readDaemonPid)();
|
|
127
|
+
const health = await (0, client_1.hostAlive)(port);
|
|
128
|
+
const running = Boolean(health);
|
|
129
|
+
return {
|
|
130
|
+
running,
|
|
131
|
+
pid: running ? (health?.pid ?? pid) : null,
|
|
132
|
+
health,
|
|
133
|
+
autoStart: isAutoStartInstalled(),
|
|
134
|
+
resolver: await (0, dns_1.createDomainResolver)().status(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function stopDaemon(env) {
|
|
138
|
+
const port = (0, paths_1.hostProxyPort)(env);
|
|
139
|
+
const health = await (0, client_1.hostAlive)(port);
|
|
140
|
+
if (health && typeof health.pid === "number") {
|
|
141
|
+
try {
|
|
142
|
+
process.kill(health.pid, "SIGTERM");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return { ok: false, error: `Could not stop Lasso Host: ${error instanceof Error ? error.message : String(error)}` };
|
|
146
|
+
}
|
|
147
|
+
const deadline = Date.now() + 8000;
|
|
148
|
+
while (Date.now() < deadline) {
|
|
149
|
+
if (!(await (0, client_1.hostAlive)(port)))
|
|
150
|
+
return { ok: true };
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
152
|
+
}
|
|
153
|
+
return { ok: false, error: "Lasso Host did not stop in time." };
|
|
154
|
+
}
|
|
155
|
+
const pid = (0, client_1.readDaemonPid)();
|
|
156
|
+
if (pid && (0, client_1.isProcessAlive)(pid)) {
|
|
157
|
+
process.kill(pid, "SIGTERM");
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, error: undefined };
|
|
161
|
+
}
|
|
162
|
+
function daemonPortMessage(env) {
|
|
163
|
+
return `http://<project>.lasso:${(0, paths_1.hostProxyPort)(env)}`;
|
|
164
|
+
}
|
|
165
|
+
function daemonHttpsPortMessage(env) {
|
|
166
|
+
return `https://<project>.lasso:${(0, paths_1.hostHttpsPort)(env)}`;
|
|
167
|
+
}
|
|
168
|
+
function yesOrNo(promptText) {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
171
|
+
rl.question(promptText, (answer) => {
|
|
172
|
+
rl.close();
|
|
173
|
+
const normalized = answer.trim().toLowerCase();
|
|
174
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
7
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const http_proxy_1 = __importDefault(require("http-proxy"));
|
|
12
|
+
const bridge_1 = require("../bridge");
|
|
13
|
+
const [cwd, publicPortArg, bridgePortArg] = process.argv.slice(2);
|
|
14
|
+
const publicPort = Number(publicPortArg) || 3000;
|
|
15
|
+
const bridgePort = Number(bridgePortArg) || 3056;
|
|
16
|
+
function freePort() {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const socket = node_net_1.default.createServer();
|
|
19
|
+
socket.once("error", reject);
|
|
20
|
+
socket.listen(0, "127.0.0.1", () => {
|
|
21
|
+
const address = socket.address();
|
|
22
|
+
if (!address || typeof address === "string")
|
|
23
|
+
return reject(new Error("Could not allocate Next.js port."));
|
|
24
|
+
socket.close(() => resolve(address.port));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
void (async () => {
|
|
29
|
+
const nextPort = await freePort();
|
|
30
|
+
const nextBin = node_path_1.default.join(cwd, "node_modules", ".bin", "next");
|
|
31
|
+
if (!node_fs_1.default.existsSync(nextBin))
|
|
32
|
+
throw new Error("Next.js is not installed in this project.");
|
|
33
|
+
const bridge = (0, bridge_1.startBridge)(cwd, null, bridgePort);
|
|
34
|
+
const nextProcess = (0, node_child_process_1.spawn)(nextBin, ["dev", "--port", String(nextPort), "--hostname", "127.0.0.1"], {
|
|
35
|
+
cwd,
|
|
36
|
+
env: { ...process.env, PORT: String(nextPort) },
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
});
|
|
39
|
+
const proxy = http_proxy_1.default.createProxyServer({ target: `http://127.0.0.1:${nextPort}`, selfHandleResponse: true, ws: true });
|
|
40
|
+
const bundlePath = node_path_1.default.resolve(__dirname, "../../overlay.js");
|
|
41
|
+
proxy.on("proxyRes", (proxyRes, _req, res) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
44
|
+
proxyRes.on("end", () => {
|
|
45
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
46
|
+
const contentType = String(proxyRes.headers["content-type"] || "");
|
|
47
|
+
const headers = { ...proxyRes.headers };
|
|
48
|
+
delete headers["content-length"];
|
|
49
|
+
res.writeHead(proxyRes.statusCode || 200, headers);
|
|
50
|
+
res.end(contentType.includes("text/html")
|
|
51
|
+
? body.replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=${bridgePort}"></script></head>`)
|
|
52
|
+
: body);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
56
|
+
if (req.url?.split("?", 1)[0] === "/__lasso/overlay.js") {
|
|
57
|
+
res.setHeader("content-type", "application/javascript");
|
|
58
|
+
res.end(node_fs_1.default.readFileSync(bundlePath, "utf8"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
proxy.web(req, res, {}, (error) => {
|
|
62
|
+
if (!res.headersSent) {
|
|
63
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
64
|
+
res.end(`Next.js is still starting: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
server.on("upgrade", (req, socket, head) => proxy.ws(req, socket, head));
|
|
69
|
+
server.listen(publicPort, "127.0.0.1");
|
|
70
|
+
const shutdown = () => {
|
|
71
|
+
bridge.close();
|
|
72
|
+
nextProcess.kill("SIGTERM");
|
|
73
|
+
proxy.close();
|
|
74
|
+
server.close(() => process.exit(0));
|
|
75
|
+
};
|
|
76
|
+
process.once("SIGTERM", shutdown);
|
|
77
|
+
process.once("SIGINT", shutdown);
|
|
78
|
+
nextProcess.once("exit", (code) => {
|
|
79
|
+
if (code && code !== 0)
|
|
80
|
+
process.exitCode = code;
|
|
81
|
+
});
|
|
82
|
+
})();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const HOST_DIR: string;
|
|
2
|
+
export declare const REGISTRY_FILE: string;
|
|
3
|
+
export declare const PID_FILE: string;
|
|
4
|
+
export declare const LOG_FILE: string;
|
|
5
|
+
export declare const DEFAULT_PROXY_PORT: number;
|
|
6
|
+
export declare const DEFAULT_HTTPS_PORT: number;
|
|
7
|
+
export declare const DEFAULT_DNS_PORT: number;
|
|
8
|
+
export declare const TLD = "lasso";
|
|
9
|
+
export declare function hostProxyPort(env: Record<string, string>): number;
|
|
10
|
+
export declare function hostDnsPort(env: Record<string, string>): number;
|
|
11
|
+
export declare function hostHttpsPort(env: Record<string, string>): number;
|
|
12
|
+
export declare function domainOf(hostname: string): string;
|
|
13
|
+
export declare function isLassoDomain(hostname: string): boolean;
|
|
14
|
+
export declare function attachHostPort(url: string, port: number): string;
|
|
15
|
+
export declare function ensureHostDir(): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TLD = exports.DEFAULT_DNS_PORT = exports.DEFAULT_HTTPS_PORT = exports.DEFAULT_PROXY_PORT = exports.LOG_FILE = exports.PID_FILE = exports.REGISTRY_FILE = exports.HOST_DIR = void 0;
|
|
7
|
+
exports.hostProxyPort = hostProxyPort;
|
|
8
|
+
exports.hostDnsPort = hostDnsPort;
|
|
9
|
+
exports.hostHttpsPort = hostHttpsPort;
|
|
10
|
+
exports.domainOf = domainOf;
|
|
11
|
+
exports.isLassoDomain = isLassoDomain;
|
|
12
|
+
exports.attachHostPort = attachHostPort;
|
|
13
|
+
exports.ensureHostDir = ensureHostDir;
|
|
14
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
exports.HOST_DIR = process.env.LASSO_HOST_DIR || node_path_1.default.join(node_os_1.default.homedir(), ".lasso", "host");
|
|
18
|
+
exports.REGISTRY_FILE = node_path_1.default.join(exports.HOST_DIR, "registry.json");
|
|
19
|
+
exports.PID_FILE = node_path_1.default.join(exports.HOST_DIR, "daemon.pid");
|
|
20
|
+
exports.LOG_FILE = node_path_1.default.join(exports.HOST_DIR, "daemon.log");
|
|
21
|
+
// Local project URLs use HTTP on 6766 and HTTPS on 6767 by default.
|
|
22
|
+
// LASSO_HOST_PORT remains available for teams that need a custom port.
|
|
23
|
+
exports.DEFAULT_PROXY_PORT = Number(process.env.LASSO_HOST_PORT) || 6766;
|
|
24
|
+
exports.DEFAULT_HTTPS_PORT = Number(process.env.LASSO_HOST_HTTPS_PORT) || 6767;
|
|
25
|
+
exports.DEFAULT_DNS_PORT = Number(process.env.LASSO_DNS_PORT) || 5358;
|
|
26
|
+
exports.TLD = "lasso";
|
|
27
|
+
function hostProxyPort(env) {
|
|
28
|
+
return Number(process.env.LASSO_HOST_PORT || env.LASSO_HOST_PORT) || exports.DEFAULT_PROXY_PORT;
|
|
29
|
+
}
|
|
30
|
+
function hostDnsPort(env) {
|
|
31
|
+
return Number(process.env.LASSO_DNS_PORT || env.LASSO_DNS_PORT) || exports.DEFAULT_DNS_PORT;
|
|
32
|
+
}
|
|
33
|
+
function hostHttpsPort(env) {
|
|
34
|
+
return Number(process.env.LASSO_HOST_HTTPS_PORT || env.LASSO_HOST_HTTPS_PORT) || exports.DEFAULT_HTTPS_PORT;
|
|
35
|
+
}
|
|
36
|
+
function domainOf(hostname) {
|
|
37
|
+
return hostname.replace(/:\d+$/, "").toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
function isLassoDomain(hostname) {
|
|
40
|
+
return domainOf(hostname).endsWith(`.${exports.TLD}`);
|
|
41
|
+
}
|
|
42
|
+
function attachHostPort(url, port) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
parsed.port = String(port);
|
|
46
|
+
return parsed.toString().replace(/\/$/, "");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function ensureHostDir() {
|
|
53
|
+
node_fs_1.default.mkdirSync(exports.HOST_DIR, { recursive: true });
|
|
54
|
+
return exports.HOST_DIR;
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface HostProject {
|
|
2
|
+
projectId: string;
|
|
3
|
+
directory: string;
|
|
4
|
+
registeredAt: string;
|
|
5
|
+
}
|
|
6
|
+
export type HostRegistry = Record<string, HostProject>;
|
|
7
|
+
export declare const DOMAIN_RE: RegExp;
|
|
8
|
+
export declare function ensureRegistryDir(): string;
|
|
9
|
+
export declare function loadRegistry(): HostRegistry;
|
|
10
|
+
export declare function saveRegistry(registry: HostRegistry): void;
|
|
11
|
+
export declare function findByDirectory(registry: HostRegistry, directory: string): {
|
|
12
|
+
domain: string;
|
|
13
|
+
project: HostProject;
|
|
14
|
+
} | null;
|
|
15
|
+
export interface RegisterInput {
|
|
16
|
+
domain: string;
|
|
17
|
+
directory: string;
|
|
18
|
+
projectId?: string;
|
|
19
|
+
registry: HostRegistry;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Registers (or re-registers) a domain → directory mapping. Never duplicates:
|
|
23
|
+
* an existing entry for the same domain is replaced, and any other domain
|
|
24
|
+
* pointing at the same directory is dropped first.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerProject(input: RegisterInput): HostRegistry;
|
|
27
|
+
export declare function unregisterDomain(registry: HostRegistry, domain: string): HostRegistry;
|
|
28
|
+
export declare function slugifyProjectName(name: string): string;
|
|
29
|
+
export declare function defaultDomainForDirectory(directory: string): string;
|
|
30
|
+
export declare function generateUniqueDomain(directory: string, registry: HostRegistry): string;
|
|
31
|
+
export declare function validateDomain(domain: string): boolean;
|