@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,103 @@
|
|
|
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.DOMAIN_RE = void 0;
|
|
7
|
+
exports.ensureRegistryDir = ensureRegistryDir;
|
|
8
|
+
exports.loadRegistry = loadRegistry;
|
|
9
|
+
exports.saveRegistry = saveRegistry;
|
|
10
|
+
exports.findByDirectory = findByDirectory;
|
|
11
|
+
exports.registerProject = registerProject;
|
|
12
|
+
exports.unregisterDomain = unregisterDomain;
|
|
13
|
+
exports.slugifyProjectName = slugifyProjectName;
|
|
14
|
+
exports.defaultDomainForDirectory = defaultDomainForDirectory;
|
|
15
|
+
exports.generateUniqueDomain = generateUniqueDomain;
|
|
16
|
+
exports.validateDomain = validateDomain;
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const paths_1 = require("./paths");
|
|
20
|
+
exports.DOMAIN_RE = new RegExp(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?\\.${paths_1.TLD}$`);
|
|
21
|
+
function ensureRegistryDir() {
|
|
22
|
+
node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true });
|
|
23
|
+
return paths_1.HOST_DIR;
|
|
24
|
+
}
|
|
25
|
+
function loadRegistry() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(paths_1.REGISTRY_FILE, "utf8"));
|
|
28
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw))
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// no registry yet
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
function saveRegistry(registry) {
|
|
37
|
+
ensureRegistryDir();
|
|
38
|
+
const temp = `${paths_1.REGISTRY_FILE}.tmp`;
|
|
39
|
+
node_fs_1.default.writeFileSync(temp, JSON.stringify(registry, null, 2) + "\n", { mode: 0o600 });
|
|
40
|
+
node_fs_1.default.renameSync(temp, paths_1.REGISTRY_FILE);
|
|
41
|
+
}
|
|
42
|
+
function findByDirectory(registry, directory) {
|
|
43
|
+
const resolved = node_path_1.default.resolve(directory);
|
|
44
|
+
for (const [domain, project] of Object.entries(registry)) {
|
|
45
|
+
if (node_path_1.default.resolve(project.directory) === resolved)
|
|
46
|
+
return { domain, project };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Registers (or re-registers) a domain → directory mapping. Never duplicates:
|
|
52
|
+
* an existing entry for the same domain is replaced, and any other domain
|
|
53
|
+
* pointing at the same directory is dropped first.
|
|
54
|
+
*/
|
|
55
|
+
function registerProject(input) {
|
|
56
|
+
const { domain, directory, projectId, registry } = input;
|
|
57
|
+
const next = {};
|
|
58
|
+
for (const [existingDomain, project] of Object.entries(registry)) {
|
|
59
|
+
if (existingDomain === domain)
|
|
60
|
+
continue;
|
|
61
|
+
if (node_path_1.default.resolve(project.directory) === node_path_1.default.resolve(directory))
|
|
62
|
+
continue;
|
|
63
|
+
next[existingDomain] = project;
|
|
64
|
+
}
|
|
65
|
+
next[domain] = {
|
|
66
|
+
projectId: projectId || "",
|
|
67
|
+
directory: node_path_1.default.resolve(directory),
|
|
68
|
+
registeredAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
function unregisterDomain(registry, domain) {
|
|
73
|
+
const next = { ...registry };
|
|
74
|
+
delete next[domain];
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function slugifyProjectName(name) {
|
|
78
|
+
const slug = name
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.replace(/\s+/g, "-")
|
|
82
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
83
|
+
.replace(/-+/g, "-")
|
|
84
|
+
.replace(/^-+|-+$/g, "");
|
|
85
|
+
return slug || "app";
|
|
86
|
+
}
|
|
87
|
+
function defaultDomainForDirectory(directory) {
|
|
88
|
+
return `${slugifyProjectName(node_path_1.default.basename(directory))}.${paths_1.TLD}`;
|
|
89
|
+
}
|
|
90
|
+
function generateUniqueDomain(directory, registry) {
|
|
91
|
+
const base = defaultDomainForDirectory(directory);
|
|
92
|
+
if (!registry[base])
|
|
93
|
+
return base;
|
|
94
|
+
for (let i = 2; i < 1000; i++) {
|
|
95
|
+
const candidate = `${base.slice(0, -(paths_1.TLD.length + 1))}-${i}.${paths_1.TLD}`;
|
|
96
|
+
if (!registry[candidate])
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
return `${base.slice(0, -(paths_1.TLD.length + 1))}-${Date.now().toString(36)}.${paths_1.TLD}`;
|
|
100
|
+
}
|
|
101
|
+
function validateDomain(domain) {
|
|
102
|
+
return exports.DOMAIN_RE.test(domain);
|
|
103
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export type RuntimeStatus = "starting" | "running" | "failed" | "crashed";
|
|
3
|
+
export interface ProjectRuntime {
|
|
4
|
+
pid: number;
|
|
5
|
+
port: number;
|
|
6
|
+
framework: string;
|
|
7
|
+
startedAt: number;
|
|
8
|
+
ready: boolean;
|
|
9
|
+
child: ChildProcess;
|
|
10
|
+
}
|
|
11
|
+
export declare function pickFreePort(): Promise<number>;
|
|
12
|
+
export declare function isReady(port: number): Promise<boolean>;
|
|
13
|
+
export interface StartRuntimeResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
runtime?: ProjectRuntime;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Starts the project's development server on a free port and waits until it
|
|
20
|
+
* answers HTTP before returning. Reuses Lasso's existing framework detection —
|
|
21
|
+
* the Host never guesses a package manager or hardcodes a runtime.
|
|
22
|
+
*/
|
|
23
|
+
export declare function startProjectRuntime(directory: string): Promise<StartRuntimeResult>;
|
|
@@ -0,0 +1,134 @@
|
|
|
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.pickFreePort = pickFreePort;
|
|
7
|
+
exports.isReady = isReady;
|
|
8
|
+
exports.startProjectRuntime = startProjectRuntime;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
11
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const framework_1 = require("../utils/framework");
|
|
15
|
+
const paths_1 = require("./paths");
|
|
16
|
+
const READY_TIMEOUT_MS = 60_000;
|
|
17
|
+
const READY_POLL_MS = 500;
|
|
18
|
+
function pickFreePort() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const server = node_net_1.default.createServer();
|
|
21
|
+
server.listen(0, "127.0.0.1", () => {
|
|
22
|
+
const address = server.address();
|
|
23
|
+
if (!address || typeof address === "string") {
|
|
24
|
+
server.close();
|
|
25
|
+
reject(new Error("Could not allocate a local port."));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const port = address.port;
|
|
29
|
+
server.close(() => resolve(port));
|
|
30
|
+
});
|
|
31
|
+
server.on("error", reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function localBin(directory, name) {
|
|
35
|
+
const bin = node_path_1.default.join(directory, "node_modules", ".bin", name);
|
|
36
|
+
return node_fs_1.default.existsSync(bin) ? bin : null;
|
|
37
|
+
}
|
|
38
|
+
function log(directory, message) {
|
|
39
|
+
try {
|
|
40
|
+
node_fs_1.default.appendFileSync(paths_1.LOG_FILE, `[${new Date().toISOString()}] ${node_path_1.default.basename(directory)}: ${message}\n`);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// logging is best-effort
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function spawnCommand(directory, port, bridgePort) {
|
|
47
|
+
const framework = (0, framework_1.detectFramework)(directory);
|
|
48
|
+
if (framework === "vite") {
|
|
49
|
+
// Launch Vite programmatically with .lasso allowlisted (Vite rejects unknown
|
|
50
|
+
// Host headers by default) while keeping it isolated in its own process.
|
|
51
|
+
const entry = node_path_1.default.join(__dirname, "vite-host-entry.js");
|
|
52
|
+
if (node_fs_1.default.existsSync(entry))
|
|
53
|
+
return { command: process.execPath, args: [entry, directory, String(port), String(bridgePort)], framework };
|
|
54
|
+
}
|
|
55
|
+
if (framework === "next") {
|
|
56
|
+
const entry = node_path_1.default.join(__dirname, "next-host-entry.js");
|
|
57
|
+
if (node_fs_1.default.existsSync(entry))
|
|
58
|
+
return { command: process.execPath, args: [entry, directory, String(port), String(bridgePort)], framework };
|
|
59
|
+
}
|
|
60
|
+
// No explicit NPM script should be invoked blindly; only framework dev bins
|
|
61
|
+
// are trusted to accept --port, so an unknown framework cannot be auto-started.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function isReady(port) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const request = httpRequest(port);
|
|
67
|
+
request.on("response", (response) => {
|
|
68
|
+
response.resume();
|
|
69
|
+
resolve(true);
|
|
70
|
+
});
|
|
71
|
+
request.on("error", () => resolve(false));
|
|
72
|
+
request.setTimeout(1500, () => {
|
|
73
|
+
request.destroy();
|
|
74
|
+
resolve(false);
|
|
75
|
+
});
|
|
76
|
+
request.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function httpRequest(port) {
|
|
80
|
+
return node_http_1.default.request({ host: "127.0.0.1", port, path: "/", method: "GET", headers: { connection: "close" } });
|
|
81
|
+
}
|
|
82
|
+
async function waitUntilReady(port) {
|
|
83
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
if (await isReady(port))
|
|
86
|
+
return true;
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS));
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Starts the project's development server on a free port and waits until it
|
|
93
|
+
* answers HTTP before returning. Reuses Lasso's existing framework detection —
|
|
94
|
+
* the Host never guesses a package manager or hardcodes a runtime.
|
|
95
|
+
*/
|
|
96
|
+
async function startProjectRuntime(directory) {
|
|
97
|
+
const port = await pickFreePort();
|
|
98
|
+
const bridgePort = await pickFreePort();
|
|
99
|
+
const plan = spawnCommand(directory, port, bridgePort);
|
|
100
|
+
if (!plan) {
|
|
101
|
+
return { ok: false, error: `${node_path_1.default.basename(directory)} is not a Vite or Next.js project, so Lasso Host can't start it automatically. Run its dev server yourself.` };
|
|
102
|
+
}
|
|
103
|
+
const child = (0, node_child_process_1.spawn)(plan.command, plan.args, {
|
|
104
|
+
cwd: directory,
|
|
105
|
+
env: { ...process.env, PORT: String(port) },
|
|
106
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
107
|
+
});
|
|
108
|
+
const runtime = {
|
|
109
|
+
pid: child.pid || 0,
|
|
110
|
+
port,
|
|
111
|
+
framework: plan.framework,
|
|
112
|
+
startedAt: Date.now(),
|
|
113
|
+
ready: false,
|
|
114
|
+
child,
|
|
115
|
+
};
|
|
116
|
+
log(directory, `starting ${plan.framework} dev server on ${port} (pid ${child.pid})`);
|
|
117
|
+
child.stdout?.on("data", (chunk) => log(directory, String(chunk).trimEnd()));
|
|
118
|
+
child.stderr?.on("data", (chunk) => log(directory, `stderr: ${String(chunk).trimEnd()}`));
|
|
119
|
+
child.on("error", (error) => {
|
|
120
|
+
log(directory, `spawn error: ${error.message}`);
|
|
121
|
+
});
|
|
122
|
+
if (child.pid === undefined) {
|
|
123
|
+
return { ok: false, error: `Could not start the project runtime for ${node_path_1.default.basename(directory)}.` };
|
|
124
|
+
}
|
|
125
|
+
const ready = await waitUntilReady(port);
|
|
126
|
+
if (!ready) {
|
|
127
|
+
child.kill("SIGTERM");
|
|
128
|
+
log(directory, "dev server did not become ready; stopping it");
|
|
129
|
+
return { ok: false, error: `${node_path_1.default.basename(directory)}'s dev server did not become ready within ${READY_TIMEOUT_MS / 1000}s. Check its logs.` };
|
|
130
|
+
}
|
|
131
|
+
runtime.ready = true;
|
|
132
|
+
log(directory, `dev server ready on ${port}`);
|
|
133
|
+
return { ok: true, runtime };
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
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_path_1 = __importDefault(require("node:path"));
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
10
|
+
const bridge_1 = require("../bridge");
|
|
11
|
+
const [cwd, portArg, bridgePortArg] = process.argv.slice(2);
|
|
12
|
+
const port = Number(portArg) || 5173;
|
|
13
|
+
const bridgePort = Number(bridgePortArg) || 3056;
|
|
14
|
+
function lassoOverlayPlugin() {
|
|
15
|
+
const bundlePath = node_path_1.default.resolve(__dirname, "../../overlay.js");
|
|
16
|
+
return {
|
|
17
|
+
name: "lasso-inject",
|
|
18
|
+
transformIndexHtml(html) {
|
|
19
|
+
return html.replace("</head>", `<script src="/__lasso/overlay.js?bridgePort=${bridgePort}"></script></head>`);
|
|
20
|
+
},
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
server.middlewares.use("/__lasso/overlay.js", (_req, res) => {
|
|
23
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
24
|
+
res.end(node_fs_1.default.readFileSync(bundlePath, "utf8"));
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
void (async () => {
|
|
30
|
+
const bridge = (0, bridge_1.startBridge)(cwd, null, bridgePort);
|
|
31
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, "package.json"));
|
|
32
|
+
const viteEntry = require.resolve("vite");
|
|
33
|
+
const { createServer } = await import((0, node_url_1.pathToFileURL)(viteEntry).href);
|
|
34
|
+
const server = await createServer({
|
|
35
|
+
root: cwd,
|
|
36
|
+
server: {
|
|
37
|
+
host: "127.0.0.1",
|
|
38
|
+
port,
|
|
39
|
+
strictPort: true,
|
|
40
|
+
allowedHosts: [
|
|
41
|
+
".lasso",
|
|
42
|
+
"lasso",
|
|
43
|
+
"localhost",
|
|
44
|
+
"127.0.0.1",
|
|
45
|
+
...(process.env.LASSO_HOST_ALLOWED_HOSTS ? process.env.LASSO_HOST_ALLOWED_HOSTS.split(",") : []),
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
plugins: [lassoOverlayPlugin()],
|
|
49
|
+
});
|
|
50
|
+
await server.listen();
|
|
51
|
+
process.on("SIGTERM", () => {
|
|
52
|
+
bridge.close();
|
|
53
|
+
void server.close().then(() => process.exit(0));
|
|
54
|
+
});
|
|
55
|
+
process.on("SIGINT", () => {
|
|
56
|
+
bridge.close();
|
|
57
|
+
void server.close().then(() => process.exit(0));
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const bridge_1 = require("./bridge");
|
|
12
|
+
const vite_1 = require("./server/vite");
|
|
13
|
+
const next_1 = require("./server/next");
|
|
14
|
+
const framework_1 = require("./utils/framework");
|
|
15
|
+
const project_1 = require("./project");
|
|
16
|
+
const auth_1 = require("./auth");
|
|
17
|
+
const daemon_1 = require("./host/daemon");
|
|
18
|
+
const paths_1 = require("./host/paths");
|
|
19
|
+
const install_1 = require("./host/install");
|
|
20
|
+
const client_1 = require("./host/client");
|
|
21
|
+
const registry_1 = require("./host/registry");
|
|
22
|
+
const VERSION = "0.1.0";
|
|
23
|
+
const program = new commander_1.Command();
|
|
24
|
+
program.name("lasso").description("Select UI in your running app, describe a change, AI edits the real source.").version(VERSION);
|
|
25
|
+
function projectEnv() {
|
|
26
|
+
return (0, project_1.readProjectEnv)(process.cwd());
|
|
27
|
+
}
|
|
28
|
+
// prettier-ignore
|
|
29
|
+
program.command("init")
|
|
30
|
+
.description("Register this app with your Lasso workspace, create lasso.config.json, and register a local .lasso domain with Lasso Host")
|
|
31
|
+
.action(async () => {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const existing = (0, project_1.readProjectConfig)(cwd);
|
|
34
|
+
if (existing) {
|
|
35
|
+
console.log(chalk_1.default.green("✓") + ` Already initialized: ${chalk_1.default.cyan(existing.id)} (from ${existing.source}).`);
|
|
36
|
+
if (existing.domain)
|
|
37
|
+
console.log(chalk_1.default.dim(` Local domain: ${chalk_1.default.cyan(existing.domain)}`));
|
|
38
|
+
console.log(chalk_1.default.dim(` Run ${chalk_1.default.cyan("lasso register")} to re-register the domain with Lasso Host.`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk_1.default.dim("Registering the project with your Lasso workspace…"));
|
|
42
|
+
const result = await (0, project_1.initProject)(cwd, projectEnv());
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log(chalk_1.default.green("✓") + ` Project created: ${chalk_1.default.cyan(result.projectId)}`);
|
|
49
|
+
if (result.domain)
|
|
50
|
+
console.log(chalk_1.default.green("✓") + ` Local domain: ${chalk_1.default.cyan(result.domain)}`);
|
|
51
|
+
if (result.workspaceId)
|
|
52
|
+
console.log(chalk_1.default.green("✓") + ` Workspace: ${chalk_1.default.cyan(result.workspaceId)}`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(chalk_1.default.dim(`Created:`));
|
|
55
|
+
console.log(chalk_1.default.dim(` ${project_1.LASSO_CONFIG_FILE} — commit it so teammates share this project.`));
|
|
56
|
+
});
|
|
57
|
+
// prettier-ignore
|
|
58
|
+
const daemon = program.command("daemon [action]")
|
|
59
|
+
.description("Start/stop Lasso Host — the local .lasso domain server (start|status|stop|restart|install|uninstall)")
|
|
60
|
+
.usage("[action]")
|
|
61
|
+
.action(async (action) => {
|
|
62
|
+
const env = projectEnv();
|
|
63
|
+
const port = (0, paths_1.hostProxyPort)(env);
|
|
64
|
+
const dnsPort = (0, paths_1.hostDnsPort)(env);
|
|
65
|
+
switch (action || "start") {
|
|
66
|
+
case "start": {
|
|
67
|
+
const result = await (0, install_1.spawnDaemon)(env);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
if (result.error)
|
|
70
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (result.alreadyRunning) {
|
|
75
|
+
console.log(chalk_1.default.dim("Lasso Host is already running."));
|
|
76
|
+
const { projects } = await (0, client_1.listHostProjects)(port);
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
console.log(chalk_1.default.green("✓") + ` ${chalk_1.default.cyan(project.domain)}`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(chalk_1.default.green("✓") + " Lasso Host started" + (result.pid ? ` (pid ${result.pid})` : "") + ".");
|
|
83
|
+
console.log(chalk_1.default.dim(` Projects become available at ${chalk_1.default.cyan(`${(0, install_1.daemonPortMessage)(env)}`)} once you run ${chalk_1.default.cyan("lasso init")} in them.`));
|
|
84
|
+
console.log(chalk_1.default.dim(` Secure URL: ${chalk_1.default.cyan((0, install_1.daemonHttpsPortMessage)(env))} · microphone/WebRTC compatible.`));
|
|
85
|
+
console.log(chalk_1.default.dim(` DNS listener: 127.0.0.1:${dnsPort} · Log: ~/.lasso/host/daemon.log`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
case "status": {
|
|
89
|
+
const report = await (0, install_1.daemonStatus)(env);
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(chalk_1.default.bold("Lasso Host"));
|
|
92
|
+
console.log(chalk_1.default.dim(` Status: ${report.running ? chalk_1.default.green("running") + (report.pid ? ` (pid ${report.pid})` : "") : chalk_1.default.red("not running")}`));
|
|
93
|
+
console.log(chalk_1.default.dim(` Proxy port: ${(0, paths_1.hostProxyPort)(env)}`));
|
|
94
|
+
console.log(chalk_1.default.dim(` HTTPS port: ${(0, paths_1.hostHttpsPort)(env)}${report.health?.https ? chalk_1.default.green(" (certificate ready)") : chalk_1.default.yellow(" (certificate unavailable)")}`));
|
|
95
|
+
console.log(chalk_1.default.dim(` DNS port: ${(0, paths_1.hostDnsPort)(env)}`));
|
|
96
|
+
console.log(chalk_1.default.dim(` Domain resolution: ${report.resolver.installed ? chalk_1.default.green("configured") : chalk_1.default.yellow("not configured")} — ${report.resolver.detail}`));
|
|
97
|
+
console.log(chalk_1.default.dim(` Automatic startup: ${report.autoStart ? chalk_1.default.green("enabled (LaunchAgent)") : chalk_1.default.yellow("disabled")}`));
|
|
98
|
+
if (report.running && report.health) {
|
|
99
|
+
console.log(chalk_1.default.dim(` Registered projects: ${report.health.registered ?? 0}`));
|
|
100
|
+
console.log(chalk_1.default.dim(` Running now: ${report.health.running ?? 0}`));
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
case "stop": {
|
|
105
|
+
const result = await (0, install_1.stopDaemon)(env);
|
|
106
|
+
if (result.error) {
|
|
107
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk_1.default.green("✓") + " Lasso Host stopped.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "restart": {
|
|
115
|
+
await (0, install_1.stopDaemon)(env);
|
|
116
|
+
const result = await (0, install_1.spawnDaemon)(env);
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
if (result.error)
|
|
119
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log(chalk_1.default.green("✓") + " Lasso Host restarted.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case "install": {
|
|
127
|
+
console.log(chalk_1.default.dim("Installing Lasso Host…"));
|
|
128
|
+
const enabled = await (0, install_1.enableAutoStart)();
|
|
129
|
+
if (enabled.error)
|
|
130
|
+
console.error(chalk_1.default.yellow("!") + ` ${enabled.error}`);
|
|
131
|
+
else
|
|
132
|
+
console.log(chalk_1.default.green("✓") + " Automatic startup configured (LaunchAgent).");
|
|
133
|
+
const resolverResult = await createResolverInstall(dnsPort);
|
|
134
|
+
if (resolverResult.ok)
|
|
135
|
+
console.log(chalk_1.default.green("✓") + ` Domain resolution: ${resolverResult.message}`);
|
|
136
|
+
else
|
|
137
|
+
console.log(chalk_1.default.yellow("!") + ` Domain resolution: ${resolverResult.message}`);
|
|
138
|
+
const spawned = await (0, install_1.spawnDaemon)(env);
|
|
139
|
+
if (spawned.ok) {
|
|
140
|
+
console.log(chalk_1.default.green("✓") + (spawned.alreadyRunning ? " Lasso Host already running." : " Lasso Host started."));
|
|
141
|
+
}
|
|
142
|
+
else if (spawned.error) {
|
|
143
|
+
console.error(chalk_1.default.yellow("!") + ` ${spawned.error}`);
|
|
144
|
+
}
|
|
145
|
+
console.log(chalk_1.default.dim(` ${(0, install_1.daemonPortMessage)(env)} (HTTP) · ${(0, install_1.daemonHttpsPortMessage)(env)} (secure) — run ${chalk_1.default.cyan("lasso init")} in a project to get a domain.`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "uninstall": {
|
|
149
|
+
const disabled = await (0, install_1.disableAutoStart)();
|
|
150
|
+
if (disabled.error)
|
|
151
|
+
console.error(chalk_1.default.yellow("!") + ` ${disabled.error}`);
|
|
152
|
+
else
|
|
153
|
+
console.log(chalk_1.default.green("✓") + " Automatic startup disabled.");
|
|
154
|
+
const stopped = await (0, install_1.stopDaemon)(env);
|
|
155
|
+
if (stopped.error)
|
|
156
|
+
console.error(chalk_1.default.yellow("!") + ` ${stopped.error}`);
|
|
157
|
+
else
|
|
158
|
+
console.log(chalk_1.default.green("✓") + " Lasso Host stopped.");
|
|
159
|
+
const resolverResult = await createResolverUninstall();
|
|
160
|
+
if (resolverResult.ok)
|
|
161
|
+
console.log(chalk_1.default.green("✓") + ` Domain resolution: ${resolverResult.message}`);
|
|
162
|
+
else
|
|
163
|
+
console.log(chalk_1.default.yellow("!") + ` Domain resolution: ${resolverResult.message}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
console.error(chalk_1.default.red("✗") + ` Unknown daemon action "${action}". Try: start | status | stop | restart | install | uninstall`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// prettier-ignore
|
|
172
|
+
program.command("_host", { hidden: true })
|
|
173
|
+
.description("Run Lasso Host in the foreground (internal)")
|
|
174
|
+
.action(async () => {
|
|
175
|
+
const env = projectEnv();
|
|
176
|
+
const result = await (0, daemon_1.startHost)({ proxyPort: (0, paths_1.hostProxyPort)(env), httpsPort: (0, paths_1.hostHttpsPort)(env), dnsPort: (0, paths_1.hostDnsPort)(env), version: VERSION });
|
|
177
|
+
if (result.error) {
|
|
178
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
(0, daemon_1.waitForShutdown)();
|
|
182
|
+
});
|
|
183
|
+
function createResolverInstall(dnsPort) {
|
|
184
|
+
// Lazy import to keep the daemon command light.
|
|
185
|
+
return import("./host/dns.js").then(({ createDomainResolver }) => createDomainResolver().install(dnsPort));
|
|
186
|
+
}
|
|
187
|
+
function createResolverUninstall() {
|
|
188
|
+
return import("./host/dns.js").then(({ createDomainResolver }) => createDomainResolver().uninstall(0));
|
|
189
|
+
}
|
|
190
|
+
// prettier-ignore
|
|
191
|
+
program.command("projects")
|
|
192
|
+
.description("List projects registered with Lasso Host and their running state")
|
|
193
|
+
.action(async () => {
|
|
194
|
+
const port = (0, paths_1.hostProxyPort)(projectEnv());
|
|
195
|
+
const { health, projects } = await (0, client_1.listHostProjects)(port);
|
|
196
|
+
if (projects.length === 0) {
|
|
197
|
+
console.log(chalk_1.default.dim("No projects registered with Lasso Host yet. Run ") + chalk_1.default.cyan("lasso init") + chalk_1.default.dim(" in a project to register one."));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log("");
|
|
201
|
+
console.log(chalk_1.default.bold("Lasso Projects") + (health ? "" : chalk_1.default.dim(" (Lasso Host is not running)")));
|
|
202
|
+
for (const project of projects) {
|
|
203
|
+
const marker = project.status === "running" ? chalk_1.default.green("●") : chalk_1.default.gray("○");
|
|
204
|
+
console.log(` ${marker} ${chalk_1.default.cyan(project.domain)}`);
|
|
205
|
+
console.log(chalk_1.default.dim(` ${project.directory}`));
|
|
206
|
+
const detail = project.status === "running" && project.port ? `Running · ${project.port}${project.pid ? ` (pid ${project.pid})` : ""}` : "Stopped";
|
|
207
|
+
console.log(` ${project.status === "running" ? chalk_1.default.green(detail) : chalk_1.default.gray(detail)}`);
|
|
208
|
+
}
|
|
209
|
+
if (!health) {
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(chalk_1.default.dim(` Start it with ${chalk_1.default.cyan("lasso daemon")}.`));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
// prettier-ignore
|
|
215
|
+
program.command("register [domain]")
|
|
216
|
+
.description("Register the current project under a local .lasso domain with Lasso Host (uses lasso.config.json or generates one)")
|
|
217
|
+
.action(async (domainArg) => {
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
const env = projectEnv();
|
|
220
|
+
const port = (0, paths_1.hostProxyPort)(env);
|
|
221
|
+
const config = (0, project_1.readProjectConfig)(cwd);
|
|
222
|
+
const projectId = config?.id || "";
|
|
223
|
+
let domain = (domainArg || "").trim().toLowerCase();
|
|
224
|
+
if (domain && !(0, registry_1.validateDomain)(domain)) {
|
|
225
|
+
console.error(chalk_1.default.red("✗") + ` "${domain}" is not a valid .lasso domain (lowercase letters, numbers, and hyphens).`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
if (!domain) {
|
|
229
|
+
if (config?.domain) {
|
|
230
|
+
domain = config.domain;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
domain = (0, registry_1.generateUniqueDomain)(cwd, (0, registry_1.loadRegistry)());
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const result = await (0, client_1.registerWithHost)(domain, cwd, projectId, port);
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
if (config && config.domain && config.domain !== domain) {
|
|
242
|
+
console.log(chalk_1.default.dim("Changed domain:"));
|
|
243
|
+
console.log(chalk_1.default.dim(` Removed: ${config.domain}`));
|
|
244
|
+
console.log(chalk_1.default.green(` Registered: ${chalk_1.default.cyan(domain)} → ${chalk_1.default.dim(cwd)}`));
|
|
245
|
+
const updated = { id: config.id, domain };
|
|
246
|
+
require("node:fs").writeFileSync(node_path_1.default.join(cwd, project_1.LASSO_CONFIG_FILE), JSON.stringify(updated, null, 2) + "\n");
|
|
247
|
+
console.log(chalk_1.default.dim(` Updated ${project_1.LASSO_CONFIG_FILE}.`));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.log(chalk_1.default.green("✓") + ` Registered ${chalk_1.default.cyan(domain)}`);
|
|
251
|
+
console.log(chalk_1.default.green("✓") + ` Points to ${chalk_1.default.dim(cwd)}`);
|
|
252
|
+
if (config && !config.domain) {
|
|
253
|
+
require("node:fs").writeFileSync(node_path_1.default.join(cwd, project_1.LASSO_CONFIG_FILE), JSON.stringify({ id: config.id, domain }, null, 2) + "\n");
|
|
254
|
+
console.log(chalk_1.default.dim(` Added domain to ${project_1.LASSO_CONFIG_FILE}.`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// prettier-ignore
|
|
259
|
+
const auth = program.command("auth").description("Sign this machine into your Lasso account (the `lasso auth` OAuth flow)");
|
|
260
|
+
// prettier-ignore
|
|
261
|
+
auth.command("login").description("Sign in with the browser OAuth flow and store a Lasso credential locally").action(async () => {
|
|
262
|
+
const cwd = process.cwd();
|
|
263
|
+
const fileEnv = (0, project_1.readProjectEnv)(cwd);
|
|
264
|
+
const result = await (0, auth_1.authLogin)(cwd, fileEnv);
|
|
265
|
+
if (!result.ok) {
|
|
266
|
+
console.error(chalk_1.default.red("✗") + ` ${result.error}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// prettier-ignore
|
|
271
|
+
auth.command("status").description("Show the Lasso credential stored on this machine").action(() => {
|
|
272
|
+
const credentials = (0, auth_1.loadCredentials)();
|
|
273
|
+
if (!credentials) {
|
|
274
|
+
console.log(chalk_1.default.dim("Not authenticated. Run ") + chalk_1.default.cyan("npx lasso auth login") + chalk_1.default.dim(" to sign in."));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const summary = (0, auth_1.credentialSummary)(credentials);
|
|
278
|
+
console.log(chalk_1.default.green("✓") + ` Authenticated as ${chalk_1.default.bold(summary.userEmail || "your account")}` + (summary.userName ? ` (${summary.userName})` : ""));
|
|
279
|
+
if (summary.workspaceName)
|
|
280
|
+
console.log(chalk_1.default.dim(`Workspace: ${summary.workspaceName} (${summary.workspaceId})`));
|
|
281
|
+
if (summary.masked)
|
|
282
|
+
console.log(chalk_1.default.dim("Credential: ") + summary.masked);
|
|
283
|
+
console.log(chalk_1.default.dim(`Stored at: ${node_path_1.default.join(node_os_1.default.homedir(), ".lasso", "credentials.json")}`));
|
|
284
|
+
});
|
|
285
|
+
// prettier-ignore
|
|
286
|
+
auth.command("logout").description("Remove the Lasso credential from this machine").action(() => {
|
|
287
|
+
if (!(0, auth_1.loadCredentials)()) {
|
|
288
|
+
console.log(chalk_1.default.dim("Not authenticated."));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
(0, auth_1.authLogout)();
|
|
292
|
+
console.log(chalk_1.default.green("✓") + " Signed out. Revoke the key from the dashboard if you no longer need it.");
|
|
293
|
+
});
|
|
294
|
+
// prettier-ignore
|
|
295
|
+
program.command("dev", { isDefault: true }).description("Start your dev server with the Lasso overlay attached").action(async () => {
|
|
296
|
+
const cwd = process.cwd();
|
|
297
|
+
const framework = (0, framework_1.detectFramework)(cwd);
|
|
298
|
+
const fileEnv = (0, project_1.readProjectEnv)(cwd);
|
|
299
|
+
const existing = (0, project_1.readProjectConfig)(cwd);
|
|
300
|
+
let collabConfig = null;
|
|
301
|
+
if (existing?.id) {
|
|
302
|
+
collabConfig = await (0, project_1.resolveCollabSession)(cwd, fileEnv, existing.id);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(chalk_1.default.yellow("!") + ` No ${chalk_1.default.bold(project_1.LASSO_CONFIG_FILE)} found. Run ${chalk_1.default.cyan("npx lasso init")} to enable realtime collaboration.`);
|
|
306
|
+
}
|
|
307
|
+
(0, bridge_1.startBridge)(cwd, collabConfig);
|
|
308
|
+
switch (framework) {
|
|
309
|
+
case "vite":
|
|
310
|
+
console.log(chalk_1.default.green("✓") + " Detected: Vite");
|
|
311
|
+
await (0, vite_1.startViteServer)(cwd);
|
|
312
|
+
break;
|
|
313
|
+
case "next":
|
|
314
|
+
console.log(chalk_1.default.green("✓") + " Detected: Next.js");
|
|
315
|
+
await (0, next_1.startNextServer)(cwd);
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
console.log(chalk_1.default.yellow("!") + " Couldn't detect a supported framework (Vite or Next.js) in this directory.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
program.parse();
|