@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.
Files changed (47) hide show
  1. package/ARCHITECTURE.md +342 -0
  2. package/CHANGELOG.md +102 -0
  3. package/CODE_OF_CONDUCT.md +129 -0
  4. package/CONTRIBUTING.md +122 -0
  5. package/LICENSE +7 -0
  6. package/README.md +548 -0
  7. package/SECURITY.md +65 -0
  8. package/SUPPORT.md +31 -0
  9. package/dist/cli/agent.d.ts +61 -0
  10. package/dist/cli/agent.js +418 -0
  11. package/dist/cli/auth.d.ts +38 -0
  12. package/dist/cli/auth.js +162 -0
  13. package/dist/cli/bridge.d.ts +160 -0
  14. package/dist/cli/bridge.js +450 -0
  15. package/dist/cli/host/certificates.d.ts +12 -0
  16. package/dist/cli/host/certificates.js +52 -0
  17. package/dist/cli/host/client.d.ts +49 -0
  18. package/dist/cli/host/client.js +141 -0
  19. package/dist/cli/host/daemon.d.ts +50 -0
  20. package/dist/cli/host/daemon.js +368 -0
  21. package/dist/cli/host/dns.d.ts +80 -0
  22. package/dist/cli/host/dns.js +255 -0
  23. package/dist/cli/host/install.d.ts +32 -0
  24. package/dist/cli/host/install.js +177 -0
  25. package/dist/cli/host/next-host-entry.d.ts +1 -0
  26. package/dist/cli/host/next-host-entry.js +82 -0
  27. package/dist/cli/host/paths.d.ts +15 -0
  28. package/dist/cli/host/paths.js +55 -0
  29. package/dist/cli/host/registry.d.ts +31 -0
  30. package/dist/cli/host/registry.js +103 -0
  31. package/dist/cli/host/runtime.d.ts +23 -0
  32. package/dist/cli/host/runtime.js +134 -0
  33. package/dist/cli/host/vite-host-entry.d.ts +1 -0
  34. package/dist/cli/host/vite-host-entry.js +59 -0
  35. package/dist/cli/index.d.ts +2 -0
  36. package/dist/cli/index.js +322 -0
  37. package/dist/cli/project.d.ts +56 -0
  38. package/dist/cli/project.js +298 -0
  39. package/dist/cli/server/next.d.ts +6 -0
  40. package/dist/cli/server/next.js +76 -0
  41. package/dist/cli/server/vite.d.ts +1 -0
  42. package/dist/cli/server/vite.js +51 -0
  43. package/dist/cli/utils/framework.d.ts +2 -0
  44. package/dist/cli/utils/framework.js +26 -0
  45. package/dist/overlay.js +20365 -0
  46. package/docs/PUBLISHING.md +63 -0
  47. 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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();