lucynui 0.0.0-stage → 0.1.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/README.md +114 -2
- package/bin/lucyn.ts +4 -0
- package/cli/create.ts +223 -0
- package/cli/index.ts +53 -0
- package/cli/postinstall.ts +64 -0
- package/cli/proc.ts +98 -0
- package/cli/ui.ts +97 -0
- package/package.json +44 -4
- package/src/build.ts +41 -0
- package/src/config.ts +54 -0
- package/src/index.ts +5 -0
- package/src/lucyn.ts +50 -0
- package/src/server.ts +162 -0
- package/webview-bun/build/libwebview-arm64.so +0 -0
- package/webview-bun/build/libwebview-x64.so +0 -0
- package/webview-bun/build/libwebview.dll +0 -0
- package/webview-bun/build/libwebview.dylib +0 -0
- package/webview-bun/src/ffi.ts +90 -0
- package/webview-bun/src/index.ts +3 -0
- package/webview-bun/src/webview.ts +346 -0
package/README.md
CHANGED
|
@@ -1,3 +1,115 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LucyN UI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Desktop apps from plain HTML, CSS and JS. Write web files, get a native window.
|
|
4
|
+
|
|
5
|
+
Built on [webview-bun](https://github.com/tr1ckydev/webview-bun) and [Elysia](https://elysiajs.com), running on Bun.
|
|
6
|
+
No React. No Rust. No Electron.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
Scaffold a new app and start it:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
lucyn create-ui myapp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or, inside an empty folder, install the package and let it set everything up:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add lucynui --trust
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Either way you end up with a dev server running and a native window on the app. The `--trust` flag matters: Bun blocks
|
|
23
|
+
install scripts by default, and this package uses one to scaffold the folder and start dev. If you already ran a plain
|
|
24
|
+
`bun add lucynui`, finish with `bun pm trust lucynui`.
|
|
25
|
+
|
|
26
|
+
A new app is just your files plus a dependency on this package:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
myapp/
|
|
30
|
+
app.ts entry point — createApp()
|
|
31
|
+
index.html your UI
|
|
32
|
+
style.css
|
|
33
|
+
app.js
|
|
34
|
+
lucyn.config.ts title / size / port
|
|
35
|
+
package.json depends on lucynui
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The framework is not copied into the app — it stays in `node_modules/lucynui`. The starter files come from the
|
|
39
|
+
[lucynui-template](https://github.com/LucyNBrowserEngine/lucynui-template) repo.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
lucyn create-ui <appname> # clone the template, install, test the build, start dev
|
|
45
|
+
lucyn # set up the current folder and start it
|
|
46
|
+
lucyn dev # dev server + window, hot reload on .html/.css/.js changes
|
|
47
|
+
lucyn build # single binary in ./dist
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`lucyn dev` serves your project root over `http://localhost:3927`, opens a native window pointed at it and reloads the
|
|
51
|
+
window whenever an `.html`, `.css` or `.js` file changes. No browser is opened.
|
|
52
|
+
|
|
53
|
+
## Config
|
|
54
|
+
|
|
55
|
+
`lucyn.config.ts` in your project root. Every field is optional.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export default {
|
|
59
|
+
title: "My App", // default: "LucyN App"
|
|
60
|
+
width: 1200, // default: 1024
|
|
61
|
+
height: 800, // default: 768
|
|
62
|
+
port: 3927, // default: 3927, falls back to the next free port
|
|
63
|
+
dev: false, // default: false
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Missing file or broken config silently falls back to the defaults, so `createApp()` always works.
|
|
68
|
+
|
|
69
|
+
You can also pass config straight to `createApp({ title: "Override" })` — anything passed there wins over the file.
|
|
70
|
+
|
|
71
|
+
## Build
|
|
72
|
+
|
|
73
|
+
`lucyn build` runs:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun build --compile --minify ./app.ts --outfile ./dist/<name>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The binary name is the `title` from `lucyn.config.ts`, lowercased (`"My App"` → `my-app`). The native window library is
|
|
80
|
+
embedded in the binary, so `./dist/<name>` is the only file you need to ship. The build halts with a message if there is
|
|
81
|
+
no `app.ts` in your project root.
|
|
82
|
+
|
|
83
|
+
## What's in the package
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
bin/lucyn.ts the CLI entry point
|
|
87
|
+
cli/ create-ui, init, dev, build, install hook, terminal styling
|
|
88
|
+
src/ the framework
|
|
89
|
+
lucyn.ts createApp — starts server + window
|
|
90
|
+
config.ts reads lucyn.config.ts
|
|
91
|
+
server.ts Elysia file server + hot reload
|
|
92
|
+
build.ts compile step
|
|
93
|
+
index.ts public exports
|
|
94
|
+
webview-bun/ native window bindings + prebuilt libraries for linux, mac and windows
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## File server
|
|
98
|
+
|
|
99
|
+
| Request | Result |
|
|
100
|
+
| ---------------- | ---------------------------- |
|
|
101
|
+
| `GET /` | `index.html` |
|
|
102
|
+
| `GET /style.css` | `style.css` |
|
|
103
|
+
| `GET /app.js` | `app.js` |
|
|
104
|
+
| `GET /*` | any file in the project root |
|
|
105
|
+
| anything missing | LucyN branded error page |
|
|
106
|
+
|
|
107
|
+
Paths are resolved inside the project root only — anything escaping it gets a 403.
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- On Windows you get a warning on startup: LucyN UI is built and tested on Linux, and Windows builds differ.
|
|
112
|
+
- `create-ui` checks for the GTK/WebKit libraries the window needs and installs them with apt on Debian-based Linux.
|
|
113
|
+
- If `index.html` is missing, the window shows a helpful error instead of a blank page.
|
|
114
|
+
- Ports: if the configured port is taken, LucyN UI keeps incrementing until it finds a free one and logs the port it
|
|
115
|
+
actually used.
|
package/bin/lucyn.ts
ADDED
package/cli/create.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { closeSync, openSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import * as proc from "./proc";
|
|
6
|
+
import * as ui from "./ui";
|
|
7
|
+
|
|
8
|
+
/** Matches the line the framework logs once the server is listening. */
|
|
9
|
+
const SERVER_READY = /dev server running on http:\/\/localhost:(\d+)/;
|
|
10
|
+
|
|
11
|
+
/** The starter files, mirroring the template repo. */
|
|
12
|
+
const TEMPLATE_FILES = ["index.html", "style.css", "app.js", "app.ts", "lucyn.config.ts", "tsconfig.json", ".gitignore"];
|
|
13
|
+
|
|
14
|
+
const bun = () => proc.findBun() ?? "bun";
|
|
15
|
+
|
|
16
|
+
/* ------------------------------------------------------------------ commands */
|
|
17
|
+
|
|
18
|
+
export async function dev(): Promise<never> {
|
|
19
|
+
proc.requireProject();
|
|
20
|
+
process.exit(await proc.run(process.cwd(), [bun(), proc.frameworkPath("lucyn.ts"), "--dev"]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function build(): Promise<never> {
|
|
24
|
+
proc.requireProject();
|
|
25
|
+
ui.info("Building your app...");
|
|
26
|
+
|
|
27
|
+
const code = await proc.run(process.cwd(), [bun(), proc.frameworkPath("build.ts")]);
|
|
28
|
+
if (code !== 0) process.exit(code);
|
|
29
|
+
|
|
30
|
+
ui.ok("Build complete! Check ./dist");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `lucyn create-ui <appname>` — the whole zero-to-running-app pipeline. */
|
|
35
|
+
export async function create(appName: string): Promise<never> {
|
|
36
|
+
if (process.platform === "win32") ui.warn(ui.WINDOWS_NOTICE);
|
|
37
|
+
|
|
38
|
+
const bunPath = await proc.ensureBun();
|
|
39
|
+
await proc.ensureSystemDeps();
|
|
40
|
+
proc.requireGit();
|
|
41
|
+
|
|
42
|
+
ui.step(1, "cloning the template, hang on");
|
|
43
|
+
await proc.runStep(process.cwd(), ["git", "clone", "--depth", "1", proc.TEMPLATE_REPO, appName]);
|
|
44
|
+
await rm(join(appName, ".git"), { recursive: true, force: true });
|
|
45
|
+
await pinFramework(appName);
|
|
46
|
+
ui.done("cloning complete — fetching the deps and required builds");
|
|
47
|
+
|
|
48
|
+
ui.step(2, "installing modules including lucynui");
|
|
49
|
+
await proc.runStep(appName, [bunPath, "install"]);
|
|
50
|
+
ui.done("modules installed");
|
|
51
|
+
|
|
52
|
+
ui.step(3, "testing build");
|
|
53
|
+
await testBuild(appName);
|
|
54
|
+
ui.done("build was successful");
|
|
55
|
+
|
|
56
|
+
ui.step(4, "starting dev with hmr");
|
|
57
|
+
const code = await runDev(appName, (port) => {
|
|
58
|
+
ui.done(`dev is up and running on port ${port}`);
|
|
59
|
+
ui.ready(basename(appName));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.exit(code);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `lucyn` / `lucyn init` — scaffold into the current folder, then run it. */
|
|
66
|
+
export async function init(): Promise<never> {
|
|
67
|
+
const bunPath = await proc.ensureBun();
|
|
68
|
+
await proc.ensureSystemDeps();
|
|
69
|
+
proc.requireGit();
|
|
70
|
+
|
|
71
|
+
if (await isProject(process.cwd())) {
|
|
72
|
+
ui.info("This is already a LucyN UI app — starting dev.");
|
|
73
|
+
} else {
|
|
74
|
+
ui.step(1, "cloning the template, hang on");
|
|
75
|
+
await scaffoldInto(process.cwd());
|
|
76
|
+
ui.done("cloning complete — fetching the deps and required builds");
|
|
77
|
+
|
|
78
|
+
ui.step(2, "installing modules including lucynui");
|
|
79
|
+
await proc.runStep(process.cwd(), [bunPath, "install"]);
|
|
80
|
+
ui.done("modules installed");
|
|
81
|
+
|
|
82
|
+
ui.step(3, "testing build");
|
|
83
|
+
await testBuild(process.cwd());
|
|
84
|
+
ui.done("build was successful");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ui.step(4, "starting dev with hmr");
|
|
88
|
+
const code = await runDev(process.cwd(), (port) => {
|
|
89
|
+
ui.done(`dev is up and running on port ${port}`);
|
|
90
|
+
ui.ready(basename(process.cwd()));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.exit(code);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ------------------------------------------------------------------- helpers */
|
|
97
|
+
|
|
98
|
+
export function isProject(dir: string) {
|
|
99
|
+
return Bun.file(join(dir, proc.CONFIG_FILE)).exists();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Runs a build and throws away the binary — the point is proving it compiles. */
|
|
103
|
+
export async function testBuild(dir: string) {
|
|
104
|
+
const code = await proc.run(dir, [bun(), proc.frameworkPath("build.ts")]);
|
|
105
|
+
await rm(join(dir, "dist"), { recursive: true, force: true });
|
|
106
|
+
|
|
107
|
+
if (code !== 0) throw new Error("the test build failed");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Copies the starter files into an existing folder, never overwriting anything. */
|
|
111
|
+
export async function scaffoldInto(dir: string) {
|
|
112
|
+
const source = await fetchTemplate();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for (const file of TEMPLATE_FILES) {
|
|
116
|
+
const target = join(dir, file);
|
|
117
|
+
if (await Bun.file(target).exists()) continue;
|
|
118
|
+
await cp(join(source, file), target);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const manifest = join(dir, "package.json");
|
|
122
|
+
if (!(await Bun.file(manifest).exists())) {
|
|
123
|
+
await cp(join(source, "package.json"), manifest);
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
await rm(source, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Points the app at this CLI's own version, so template drift can never bite. */
|
|
131
|
+
async function pinFramework(dir: string) {
|
|
132
|
+
const manifest = Bun.file(join(dir, "package.json"));
|
|
133
|
+
if (!(await manifest.exists())) return;
|
|
134
|
+
|
|
135
|
+
// Running from a checkout rather than an install? Point the app at this copy
|
|
136
|
+
// so the whole flow can be tried out before the package is published.
|
|
137
|
+
const spec = import.meta.dir.includes("node_modules")
|
|
138
|
+
? `^${await proc.packageVersion()}`
|
|
139
|
+
: `file:${proc.packageRoot()}`;
|
|
140
|
+
|
|
141
|
+
const pkg = await manifest.json();
|
|
142
|
+
pkg.dependencies = { ...(pkg.dependencies ?? {}), lucynui: spec };
|
|
143
|
+
await Bun.write(manifest, JSON.stringify(pkg, null, 2) + "\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function fetchTemplate() {
|
|
147
|
+
const tmp = await mkdtemp(join(tmpdir(), "lucyn-template-"));
|
|
148
|
+
await mkdir(tmp, { recursive: true });
|
|
149
|
+
const checkout = join(tmp, "template");
|
|
150
|
+
|
|
151
|
+
await proc.runStep(tmp, ["git", "clone", "--depth", "1", proc.TEMPLATE_REPO, checkout]);
|
|
152
|
+
await rm(join(checkout, ".git"), { recursive: true, force: true });
|
|
153
|
+
|
|
154
|
+
return checkout;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Starts dev in the foreground, forwarding output, and calls `onReady` with the
|
|
159
|
+
* port the server actually bound to.
|
|
160
|
+
*/
|
|
161
|
+
async function runDev(dir: string, onReady: (port: number) => void): Promise<number> {
|
|
162
|
+
const child = Bun.spawn([bun(), proc.frameworkPath("lucyn.ts"), "--dev"], {
|
|
163
|
+
cwd: dir,
|
|
164
|
+
stdin: "ignore",
|
|
165
|
+
stdout: "pipe",
|
|
166
|
+
stderr: "inherit",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const decoder = new TextDecoder();
|
|
170
|
+
let seen = "";
|
|
171
|
+
let announced = false;
|
|
172
|
+
|
|
173
|
+
for await (const chunk of child.stdout) {
|
|
174
|
+
process.stdout.write(chunk);
|
|
175
|
+
if (announced) continue;
|
|
176
|
+
|
|
177
|
+
seen += decoder.decode(chunk, { stream: true });
|
|
178
|
+
const port = SERVER_READY.exec(seen)?.[1];
|
|
179
|
+
if (port) {
|
|
180
|
+
announced = true;
|
|
181
|
+
onReady(Number(port));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return await child.exited;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Starts dev detached so the caller can exit, writing output to `logPath`.
|
|
190
|
+
* Returns the port, or null when the window never came up.
|
|
191
|
+
*/
|
|
192
|
+
export async function devDetached(dir: string, logPath: string): Promise<number | null> {
|
|
193
|
+
const fd = openSync(logPath, "a");
|
|
194
|
+
|
|
195
|
+
const child = Bun.spawn([bun(), proc.frameworkPath("lucyn.ts"), "--dev"], {
|
|
196
|
+
cwd: dir,
|
|
197
|
+
stdin: "ignore",
|
|
198
|
+
stdout: fd,
|
|
199
|
+
stderr: fd,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
for (let attempt = 0; attempt < 80; attempt++) {
|
|
204
|
+
await Bun.sleep(250);
|
|
205
|
+
|
|
206
|
+
const log = await Bun.file(logPath).text();
|
|
207
|
+
const port = SERVER_READY.exec(log)?.[1];
|
|
208
|
+
|
|
209
|
+
if (port) {
|
|
210
|
+
// Give it a moment to prove it survives the window actually opening.
|
|
211
|
+
await Bun.sleep(500);
|
|
212
|
+
return child.exitCode === null ? Number(port) : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (child.exitCode !== null) return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
} finally {
|
|
220
|
+
child.unref();
|
|
221
|
+
closeSync(fd);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as create from "./create";
|
|
2
|
+
import * as ui from "./ui";
|
|
3
|
+
|
|
4
|
+
const HELP = `${ui.accent("lucyn")} — desktop apps from plain HTML, CSS and JS
|
|
5
|
+
|
|
6
|
+
${ui.accent("lucyn create-ui <appname>")} scaffold a new app and start it
|
|
7
|
+
${ui.accent("lucyn")} set up the current folder and start it
|
|
8
|
+
${ui.accent("lucyn dev")} start dev mode with hot reload
|
|
9
|
+
${ui.accent("lucyn build")} build your app into a single binary
|
|
10
|
+
|
|
11
|
+
Inside an empty folder, ${ui.accent("bun add lucynui --trust")} runs all four steps on its own.
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export async function run(argv: string[]): Promise<void> {
|
|
15
|
+
const [command, ...rest] = argv;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
switch (command) {
|
|
19
|
+
case "create-ui":
|
|
20
|
+
case "create": {
|
|
21
|
+
const appName = rest[0];
|
|
22
|
+
if (!appName) throw new Error("usage: lucyn create-ui <appname>");
|
|
23
|
+
await create.create(appName);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case "dev":
|
|
28
|
+
await create.dev();
|
|
29
|
+
return;
|
|
30
|
+
|
|
31
|
+
case "build":
|
|
32
|
+
await create.build();
|
|
33
|
+
return;
|
|
34
|
+
|
|
35
|
+
case "init":
|
|
36
|
+
case undefined:
|
|
37
|
+
await create.init();
|
|
38
|
+
return;
|
|
39
|
+
|
|
40
|
+
case "help":
|
|
41
|
+
case "--help":
|
|
42
|
+
case "-h":
|
|
43
|
+
console.log(HELP);
|
|
44
|
+
return;
|
|
45
|
+
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`unknown command: ${command} — try lucyn help`);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
ui.fail(error instanceof Error ? error.message : String(error));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
import * as create from "./create";
|
|
4
|
+
import * as ui from "./ui";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs after `bun add lucynui --trust` / `npm i lucynui`.
|
|
8
|
+
*
|
|
9
|
+
* When the folder is empty it scaffolds the starter app, proves the build works
|
|
10
|
+
* and leaves a dev server running before the install command returns. Any folder
|
|
11
|
+
* that already looks like a project is left completely alone.
|
|
12
|
+
*/
|
|
13
|
+
export async function postinstall(): Promise<void> {
|
|
14
|
+
// Only when installed as a dependency — not while developing this package.
|
|
15
|
+
if (!import.meta.dir.includes("node_modules")) return;
|
|
16
|
+
if (process.env.LUCYN_SKIP_POSTINSTALL) return;
|
|
17
|
+
|
|
18
|
+
const dir = projectDir();
|
|
19
|
+
if (!dir) return;
|
|
20
|
+
|
|
21
|
+
if (await create.isProject(dir)) return;
|
|
22
|
+
if (existsSync(join(dir, "index.html")) || existsSync(join(dir, "src"))) return;
|
|
23
|
+
|
|
24
|
+
ui.useTerminal();
|
|
25
|
+
ui.info("LucyN UI found an empty project and is setting it up.");
|
|
26
|
+
ui.line();
|
|
27
|
+
|
|
28
|
+
ui.step(1, "cloning the template, hang on");
|
|
29
|
+
await create.scaffoldInto(dir);
|
|
30
|
+
ui.done("cloning complete — fetching the deps and required builds");
|
|
31
|
+
|
|
32
|
+
ui.step(2, "installing modules including lucynui");
|
|
33
|
+
ui.done("modules installed");
|
|
34
|
+
|
|
35
|
+
ui.step(3, "testing build");
|
|
36
|
+
await create.testBuild(dir);
|
|
37
|
+
ui.done("build was successful");
|
|
38
|
+
|
|
39
|
+
ui.step(4, "starting dev with hmr");
|
|
40
|
+
const logPath = join(dir, ".lucyn-dev.log");
|
|
41
|
+
const port = await create.devDetached(dir, logPath);
|
|
42
|
+
|
|
43
|
+
if (port === null) {
|
|
44
|
+
ui.warn(`The window did not come up. Run lucyn dev yourself — log: ${logPath}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ui.done(`dev is up and running on port ${port}`);
|
|
49
|
+
ui.line();
|
|
50
|
+
ui.line(` ${ui.accent("Log:")} ${ui.text(logPath)}`);
|
|
51
|
+
ui.line();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The folder the user ran `bun add` in — the one holding `node_modules`. */
|
|
55
|
+
function projectDir(): string | null {
|
|
56
|
+
const fromEnv = process.env.INIT_CWD ?? process.env.npm_config_local_prefix;
|
|
57
|
+
if (fromEnv) return fromEnv;
|
|
58
|
+
|
|
59
|
+
const marker = `${sep}node_modules${sep}`;
|
|
60
|
+
const index = import.meta.dir.lastIndexOf(marker);
|
|
61
|
+
return index === -1 ? null : import.meta.dir.slice(0, index);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (import.meta.main) await postinstall();
|
package/cli/proc.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import * as ui from "./ui";
|
|
6
|
+
|
|
7
|
+
/** Marks the root of a LucyN UI project. */
|
|
8
|
+
export const CONFIG_FILE = "lucyn.config.ts";
|
|
9
|
+
|
|
10
|
+
/** Where `create-ui` clones the starter app from. */
|
|
11
|
+
export const TEMPLATE_REPO = "https://github.com/LucyNBrowserEngine/lucynui-template";
|
|
12
|
+
|
|
13
|
+
/** Absolute path to the package root, wherever it is installed. */
|
|
14
|
+
export function packageRoot() {
|
|
15
|
+
return fileURLToPath(new URL("..", import.meta.url));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Absolute path to a file inside the framework's own `src/`, wherever the package is installed. */
|
|
19
|
+
export function frameworkPath(file: string) {
|
|
20
|
+
return fileURLToPath(new URL(`../src/${file}`, import.meta.url));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function packageVersion() {
|
|
24
|
+
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
25
|
+
return String(pkg.version ?? "0.0.0");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Bun on PATH, or in the default install location. */
|
|
29
|
+
export function findBun(): string | null {
|
|
30
|
+
const onPath = Bun.which("bun");
|
|
31
|
+
if (onPath) return onPath;
|
|
32
|
+
|
|
33
|
+
const fallback = join(homedir(), ".bun", "bin", "bun");
|
|
34
|
+
return existsSync(fallback) ? fallback : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function requireProject() {
|
|
38
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
39
|
+
throw new Error("No lucyn.config.ts found. Are you inside a LucyN UI project?");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs a program with its output going straight to the terminal and returns its exit code.
|
|
45
|
+
* `stdin` is only inherited for commands that need to prompt the user.
|
|
46
|
+
*/
|
|
47
|
+
export async function run(dir: string, cmd: string[], stdin: "ignore" | "inherit" = "ignore"): Promise<number> {
|
|
48
|
+
const head = cmd[0];
|
|
49
|
+
if (!head) throw new Error("empty command");
|
|
50
|
+
|
|
51
|
+
let child;
|
|
52
|
+
try {
|
|
53
|
+
child = Bun.spawn(cmd, { cwd: dir, stdin, stdout: "inherit", stderr: "inherit" });
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(`could not run ${head} — is it installed?`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return await child.exited;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runStep(dir: string, cmd: string[], stdin: "ignore" | "inherit" = "ignore") {
|
|
62
|
+
const code = await run(dir, cmd, stdin);
|
|
63
|
+
if (code !== 0) throw new Error(`${cmd.join(" ")} failed (exit code ${code})`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Returns bun, installing it first when it is missing. */
|
|
67
|
+
export async function ensureBun(): Promise<string> {
|
|
68
|
+
const found = findBun();
|
|
69
|
+
if (found) return found;
|
|
70
|
+
|
|
71
|
+
ui.info("Bun not found. Installing bun...");
|
|
72
|
+
await runStep(process.cwd(), ["bash", "-c", "curl -fsSL https://bun.sh/install | bash"]);
|
|
73
|
+
|
|
74
|
+
const installed = findBun();
|
|
75
|
+
if (!installed) {
|
|
76
|
+
throw new Error("bun was installed but is not on PATH yet — restart your shell and run lucyn again");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ui.ok("Bun installed successfully");
|
|
80
|
+
return installed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Installs the GTK/WebKit libraries the window needs. Linux only. */
|
|
84
|
+
export async function ensureSystemDeps() {
|
|
85
|
+
if (process.platform !== "linux") return;
|
|
86
|
+
if (!Bun.which("dpkg")) return;
|
|
87
|
+
|
|
88
|
+
const probe = Bun.spawnSync(["dpkg", "-l", "libwebkitgtk-6.0-4"]);
|
|
89
|
+
if (probe.exitCode === 0 && probe.stdout.toString().includes("ii")) return;
|
|
90
|
+
|
|
91
|
+
ui.info("Installing system dependencies...");
|
|
92
|
+
await runStep(process.cwd(), ["sudo", "apt", "install", "-y", "libgtk-4-1", "libwebkitgtk-6.0-4"], "inherit");
|
|
93
|
+
ui.ok("Dependencies installed");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function requireGit() {
|
|
97
|
+
if (!Bun.which("git")) throw new Error("git is required to fetch the template");
|
|
98
|
+
}
|
package/cli/ui.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Every line the LucyN CLI prints goes through here. */
|
|
2
|
+
|
|
3
|
+
import { openSync, writeSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
type Rgb = readonly [number, number, number];
|
|
6
|
+
|
|
7
|
+
const TEXT: Rgb = [0xe8, 0xea, 0xf0];
|
|
8
|
+
const GOLD: Rgb = [0xe0, 0xb3, 0x4d];
|
|
9
|
+
const ERROR: Rgb = [0xff, 0x6b, 0x6b];
|
|
10
|
+
const SUCCESS: Rgb = [0x69, 0xdb, 0x7c];
|
|
11
|
+
|
|
12
|
+
const RESET = "\x1b[0m";
|
|
13
|
+
|
|
14
|
+
function paint([r, g, b]: Rgb, value: string) {
|
|
15
|
+
return `\x1b[38;2;${r};${g};${b}m${value}${RESET}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const text = (value: string) => paint(TEXT, value);
|
|
19
|
+
export const gold = (value: string) => paint(GOLD, value);
|
|
20
|
+
export const error = (value: string) => paint(ERROR, value);
|
|
21
|
+
export const success = (value: string) => paint(SUCCESS, value);
|
|
22
|
+
/** Gold and bold — used for labels and commands. */
|
|
23
|
+
export const accent = (value: string) => `\x1b[1m${paint(GOLD, value)}`;
|
|
24
|
+
|
|
25
|
+
let sink = (value: string) => console.log(value);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sends everything straight to the terminal.
|
|
29
|
+
*
|
|
30
|
+
* Needed when we run as an install script: package managers swallow the streams
|
|
31
|
+
* of lifecycle scripts, so without this the user stares at a blank screen.
|
|
32
|
+
*/
|
|
33
|
+
export function useTerminal() {
|
|
34
|
+
try {
|
|
35
|
+
const fd = openSync("/dev/tty", "a");
|
|
36
|
+
sink = (value: string) => writeSync(fd, value + "\n");
|
|
37
|
+
} catch {
|
|
38
|
+
// No terminal to talk to — stdout is the best we have.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function emit(value: string) {
|
|
43
|
+
sink(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const info = (message: string) => emit(text(message));
|
|
47
|
+
export const warn = (message: string) => emit(gold(message));
|
|
48
|
+
export const ok = (message: string) => emit(success(message));
|
|
49
|
+
export const fail = (message: string) => emit(error(message));
|
|
50
|
+
/** A blank line. */
|
|
51
|
+
export const line = (value = "") => emit(value);
|
|
52
|
+
|
|
53
|
+
/** The Windows warning, printed before anything else. */
|
|
54
|
+
export const WINDOWS_NOTICE = `You are on Windows. LucyN UI on Windows is not top grade
|
|
55
|
+
as Windows builds differ from Linux where the framework
|
|
56
|
+
was made and tested. If you encounter any bugs or build
|
|
57
|
+
errors please dont hesitate to open an issue at the LucyN
|
|
58
|
+
org. Thank you.`;
|
|
59
|
+
|
|
60
|
+
/** Prints a numbered step, e.g. `1. cloning the template`. */
|
|
61
|
+
export function step(index: number, message: string) {
|
|
62
|
+
emit(`${accent(`${index}.`)} ${text(message)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Prints the result of the step above it. */
|
|
66
|
+
export function done(message: string) {
|
|
67
|
+
emit(` ${success("✓")} ${text(message)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const BANNER_WIDTH = 32;
|
|
71
|
+
|
|
72
|
+
function centered(value: string, width: number) {
|
|
73
|
+
if (value.length >= width) return value;
|
|
74
|
+
const left = Math.floor((width - value.length) / 2);
|
|
75
|
+
return " ".repeat(left) + value + " ".repeat(width - value.length - left);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The gold double-bordered box drawn once a new app is running. */
|
|
79
|
+
export function banner(value: string) {
|
|
80
|
+
const inner = ` ${centered(value, BANNER_WIDTH)} `;
|
|
81
|
+
const edge = "═".repeat(inner.length);
|
|
82
|
+
return [gold(`╔${edge}╗`), gold("║") + accent(inner) + gold("║"), gold(`╚${edge}╝`)].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The closing message after a successful scaffold. */
|
|
86
|
+
export function ready(appName: string) {
|
|
87
|
+
line();
|
|
88
|
+
emit(banner("LucyN UI Ready!"));
|
|
89
|
+
line();
|
|
90
|
+
emit(` ${accent("App:")} ${text(appName)}`);
|
|
91
|
+
line();
|
|
92
|
+
emit(text(" Next steps:"));
|
|
93
|
+
line();
|
|
94
|
+
emit(" " + accent(`cd ${appName}`));
|
|
95
|
+
emit(" " + accent("lucyn dev"));
|
|
96
|
+
line();
|
|
97
|
+
}
|