ucode-agent 1.6.0 → 1.8.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/src/ui/theme.js CHANGED
@@ -252,7 +252,11 @@ export function asLabel(text) {
252
252
  return String(text ?? '')
253
253
  .trim()
254
254
  .replace(/\s+/g, ' ')
255
- .replace(/(?<=[\w)\]"'`])[.。]+$/, '');
255
+ // A trailing stop, from a model's sentence or a tool's own output
256
+ // ("Building…", "Completing…"), is noise on a one-line label. A dot that
257
+ // is the argument itself — "Listing ." — is not, so a word has to come
258
+ // before it.
259
+ .replace(/(?<=[\w)\]"'`])[.。…]+$/, '');
256
260
  }
257
261
 
258
262
  /**
@@ -272,3 +276,47 @@ export function planLine(items) {
272
276
  });
273
277
  return ` ${sky(`plan ${done}/${list.length}`)} ${parts.join(dim(' · '))}`;
274
278
  }
279
+
280
+ /**
281
+ * The background ucode paints behind itself.
282
+ *
283
+ * A terminal's own background is whatever the person set it to years ago:
284
+ * white, solarized, a photograph. The interface was drawn for a dark one, and
285
+ * on a light terminal the dim greys it relies on turn to near-invisible smoke.
286
+ * So ucode paints its own ground for as long as it is running, and the
287
+ * alternate screen gives it back untouched on exit.
288
+ *
289
+ * Near-black rather than black: a true #000 against a bright room is a hole,
290
+ * and the box edges lose their softness. This is the shade a code editor
291
+ * settles on for the same reason.
292
+ */
293
+ export const BACKGROUND = process.env.UCODE_BG || '#131316';
294
+
295
+ const rgb = (hex) => {
296
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex.trim());
297
+ return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [19, 19, 22];
298
+ };
299
+
300
+ /** Turn the background on. Everything painted after this sits on it. */
301
+ export const BG_ON = (() => {
302
+ if (process.env.NO_COLOR || process.env.UCODE_BG === 'off') return '';
303
+ const [r, g, b] = rgb(BACKGROUND);
304
+ return `\x1b[48;2;${r};${g};${b}m`;
305
+ })();
306
+
307
+ /** Hand the terminal its own colours back. */
308
+ export const BG_OFF = BG_ON ? '\x1b[0m' : '';
309
+
310
+ /**
311
+ * Keep the background on across a line that resets it.
312
+ *
313
+ * chalk closes a foreground with 39 and a background with 49, and 49 means
314
+ * "the terminal's default" — which is exactly the colour being painted over.
315
+ * A diff line, which sets its own background, would therefore punch a hole
316
+ * through to the terminal's ground for the rest of the line. Re-asserting the
317
+ * background after every reset closes those holes.
318
+ */
319
+ export function onBackground(text) {
320
+ if (!BG_ON) return text;
321
+ return BG_ON + String(text).replace(/\x1b\[(?:0|49)m/g, (m) => m + BG_ON);
322
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { ReactNode, useState } from "react";
4
+ import Link from "next/link";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
7
+
8
+ export type NavItem = { href: string; label: string; icon?: ReactNode };
9
+
10
+ /**
11
+ * The frame every page sits in: a sidebar on a wide screen, the same nav
12
+ * behind a button on a phone. One list of links drives both, so they cannot
13
+ * drift apart.
14
+ */
15
+ export function AppShell({
16
+ nav,
17
+ current,
18
+ title,
19
+ children,
20
+ }: {
21
+ nav: NavItem[];
22
+ current?: string;
23
+ title: string;
24
+ children: ReactNode;
25
+ }) {
26
+ const [open, setOpen] = useState(false);
27
+
28
+ const links = (onNavigate?: () => void) => (
29
+ <nav className="space-y-1" aria-label="Main">
30
+ {nav.map((item) => {
31
+ const active = current === item.href;
32
+ return (
33
+ <Link
34
+ key={item.href}
35
+ href={item.href}
36
+ onClick={onNavigate}
37
+ aria-current={active ? "page" : undefined}
38
+ className={
39
+ active
40
+ ? "flex items-center gap-3 rounded-md bg-muted px-3 py-2 text-sm font-medium"
41
+ : "flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground"
42
+ }
43
+ >
44
+ {item.icon}
45
+ {item.label}
46
+ </Link>
47
+ );
48
+ })}
49
+ </nav>
50
+ );
51
+
52
+ return (
53
+ <div className="flex min-h-svh">
54
+ <aside className="hidden w-60 shrink-0 border-r p-4 md:block">
55
+ <div className="mb-6 px-3 text-sm font-semibold tracking-tight">{title}</div>
56
+ {links()}
57
+ </aside>
58
+
59
+ <div className="flex min-w-0 flex-1 flex-col">
60
+ <header className="flex h-14 items-center gap-3 border-b px-4 md:px-6">
61
+ <Sheet open={open} onOpenChange={setOpen}>
62
+ <SheetTrigger asChild>
63
+ <Button variant="ghost" size="sm" className="md:hidden" aria-label="Open menu">
64
+ Menu
65
+ </Button>
66
+ </SheetTrigger>
67
+ <SheetContent side="left" className="w-64 p-4">
68
+ <SheetTitle className="mb-6 px-3 text-sm font-semibold">{title}</SheetTitle>
69
+ {links(() => setOpen(false))}
70
+ </SheetContent>
71
+ </Sheet>
72
+ <span className="text-sm font-medium md:hidden">{title}</span>
73
+ </header>
74
+
75
+ <main className="min-w-0 flex-1 p-4 md:p-8">
76
+ <div className="mx-auto w-full max-w-6xl space-y-8">{children}</div>
77
+ </main>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState, ReactNode } from "react";
4
+ import { Input } from "@/components/ui/input";
5
+ import {
6
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
7
+ } from "@/components/ui/table";
8
+
9
+ export type Column<T> = {
10
+ key: keyof T & string;
11
+ header: string;
12
+ /** Right-align and tabular-nums, for money and counts. */
13
+ numeric?: boolean;
14
+ render?: (row: T) => ReactNode;
15
+ };
16
+
17
+ /**
18
+ * A table of things you can search and sort.
19
+ *
20
+ * Sorting and filtering happen here, over rows already in memory: it is the
21
+ * right shape up to a few thousand rows and the wrong one past that, where
22
+ * the server should be doing both.
23
+ */
24
+ export function DataTable<T extends { id: string | number }>({
25
+ rows,
26
+ columns,
27
+ searchPlaceholder = "Search…",
28
+ empty,
29
+ }: {
30
+ rows: T[];
31
+ columns: Column<T>[];
32
+ searchPlaceholder?: string;
33
+ empty?: ReactNode;
34
+ }) {
35
+ const [query, setQuery] = useState("");
36
+ const [sort, setSort] = useState<{ key: string; asc: boolean } | null>(null);
37
+
38
+ const shown = useMemo(() => {
39
+ const needle = query.trim().toLowerCase();
40
+ let out = needle
41
+ ? rows.filter((row) =>
42
+ columns.some((c) => String(row[c.key] ?? "").toLowerCase().includes(needle)))
43
+ : rows.slice();
44
+ if (sort) {
45
+ out.sort((a, b) => {
46
+ const x = a[sort.key as keyof T];
47
+ const y = b[sort.key as keyof T];
48
+ if (typeof x === "number" && typeof y === "number") return sort.asc ? x - y : y - x;
49
+ return sort.asc
50
+ ? String(x ?? "").localeCompare(String(y ?? ""))
51
+ : String(y ?? "").localeCompare(String(x ?? ""));
52
+ });
53
+ }
54
+ return out;
55
+ }, [rows, columns, query, sort]);
56
+
57
+ const toggle = (key: string) =>
58
+ setSort((s) => (s?.key === key ? { key, asc: !s.asc } : { key, asc: true }));
59
+
60
+ return (
61
+ <div className="space-y-4">
62
+ <Input
63
+ value={query}
64
+ onChange={(e) => setQuery(e.target.value)}
65
+ placeholder={searchPlaceholder}
66
+ className="max-w-xs"
67
+ aria-label={searchPlaceholder}
68
+ />
69
+
70
+ <div className="overflow-x-auto rounded-lg border">
71
+ <Table>
72
+ <TableHeader>
73
+ <TableRow>
74
+ {columns.map((c) => (
75
+ <TableHead key={c.key} className={c.numeric ? "text-right" : undefined}>
76
+ <button
77
+ type="button"
78
+ onClick={() => toggle(c.key)}
79
+ className="inline-flex items-center gap-1 hover:text-foreground"
80
+ aria-label={`Sort by ${c.header}`}
81
+ >
82
+ {c.header}
83
+ <span aria-hidden className="text-xs text-muted-foreground">
84
+ {sort?.key === c.key ? (sort.asc ? "↑" : "↓") : ""}
85
+ </span>
86
+ </button>
87
+ </TableHead>
88
+ ))}
89
+ </TableRow>
90
+ </TableHeader>
91
+ <TableBody>
92
+ {shown.length === 0 ? (
93
+ <TableRow>
94
+ <TableCell colSpan={columns.length} className="h-28 text-center text-sm text-muted-foreground">
95
+ {query ? `Nothing matches “${query}”.` : empty ?? "Nothing here yet."}
96
+ </TableCell>
97
+ </TableRow>
98
+ ) : (
99
+ shown.map((row) => (
100
+ <TableRow key={row.id}>
101
+ {columns.map((c) => (
102
+ <TableCell
103
+ key={c.key}
104
+ className={c.numeric ? "text-right tabular-nums" : undefined}
105
+ >
106
+ {c.render ? c.render(row) : String(row[c.key] ?? "")}
107
+ </TableCell>
108
+ ))}
109
+ </TableRow>
110
+ ))
111
+ )}
112
+ </TableBody>
113
+ </Table>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,41 @@
1
+ import { ReactNode } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+
4
+ /**
5
+ * What a list looks like before anything is in it.
6
+ *
7
+ * An empty screen is where people decide whether a product is working or
8
+ * broken, so this says which it is and offers the one action that fills it.
9
+ */
10
+ export function EmptyState({
11
+ icon,
12
+ title,
13
+ description,
14
+ actionLabel,
15
+ onAction,
16
+ }: {
17
+ icon?: ReactNode;
18
+ title: string;
19
+ description?: string;
20
+ actionLabel?: string;
21
+ onAction?: () => void;
22
+ }) {
23
+ return (
24
+ <div className="flex flex-col items-center justify-center rounded-lg border border-dashed px-6 py-16 text-center">
25
+ {icon ? (
26
+ <div className="mb-4 flex size-11 items-center justify-center rounded-full bg-muted text-muted-foreground">
27
+ {icon}
28
+ </div>
29
+ ) : null}
30
+ <h3 className="text-base font-medium">{title}</h3>
31
+ {description ? (
32
+ <p className="mt-1.5 max-w-sm text-sm text-muted-foreground text-pretty">{description}</p>
33
+ ) : null}
34
+ {actionLabel ? (
35
+ <Button className="mt-6" onClick={onAction}>
36
+ {actionLabel}
37
+ </Button>
38
+ ) : null}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,27 @@
1
+ import { ReactNode } from "react";
2
+
3
+ /**
4
+ * The top of a page: what it is, what it is for, and what you can do here.
5
+ * Actions sit on the right on a wide screen and wrap underneath on a phone.
6
+ */
7
+ export function PageHeader({
8
+ title,
9
+ description,
10
+ actions,
11
+ }: {
12
+ title: string;
13
+ description?: string;
14
+ actions?: ReactNode;
15
+ }) {
16
+ return (
17
+ <div className="flex flex-col gap-4 border-b pb-6 sm:flex-row sm:items-end sm:justify-between">
18
+ <div className="space-y-1.5">
19
+ <h1 className="text-2xl font-semibold tracking-tight text-balance">{title}</h1>
20
+ {description ? (
21
+ <p className="max-w-2xl text-sm text-muted-foreground text-pretty">{description}</p>
22
+ ) : null}
23
+ </div>
24
+ {actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,46 @@
1
+ import { Card, CardContent } from "@/components/ui/card";
2
+
3
+ export type Stat = {
4
+ label: string;
5
+ value: string;
6
+ /** Change since the last period, e.g. 12 or -3.4. Omit for no delta. */
7
+ change?: number;
8
+ hint?: string;
9
+ };
10
+
11
+ /**
12
+ * The row of numbers at the top of a dashboard.
13
+ *
14
+ * A number on its own says nothing, so each one carries what it is measured
15
+ * against. Rising is not always good, so the colour follows the sign and the
16
+ * caller words the label.
17
+ */
18
+ export function StatCards({ stats }: { stats: Stat[] }) {
19
+ return (
20
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
21
+ {stats.map((stat) => (
22
+ <Card key={stat.label}>
23
+ <CardContent className="space-y-2 p-5">
24
+ <p className="text-sm font-medium text-muted-foreground">{stat.label}</p>
25
+ <div className="flex items-baseline gap-2">
26
+ <span className="text-2xl font-semibold tabular-nums tracking-tight">{stat.value}</span>
27
+ {stat.change !== undefined ? (
28
+ <span
29
+ className={
30
+ stat.change >= 0
31
+ ? "text-xs font-medium text-emerald-600 dark:text-emerald-400"
32
+ : "text-xs font-medium text-rose-600 dark:text-rose-400"
33
+ }
34
+ >
35
+ {stat.change >= 0 ? "+" : ""}
36
+ {stat.change}%
37
+ </span>
38
+ ) : null}
39
+ </div>
40
+ {stat.hint ? <p className="text-xs text-muted-foreground">{stat.hint}</p> : null}
41
+ </CardContent>
42
+ </Card>
43
+ ))}
44
+ </div>
45
+ );
46
+ }
package/ucode.js CHANGED
@@ -1,124 +1,132 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ucode.js — the command.
4
- *
5
- * Parses the arguments, builds an Agent, and gets out of the way. Everything
6
- * of substance is under src/.
7
- */
8
-
9
- import path from 'node:path';
10
- import process from 'node:process';
11
- import { readFile } from 'node:fs/promises';
12
- import { realpathSync } from 'node:fs';
13
- import { pathToFileURL, fileURLToPath } from 'node:url';
14
-
15
- import { Agent } from './src/core/loop.js';
16
- import { setModel, modelName, MODELS, DEFAULT_MODEL, ENV_FILE } from './src/core/provider.js';
17
- import { VERSION } from './src/core/version.js';
18
- import { Plain } from './src/ui/plain.js';
19
- import { blue, dim, sky } from './src/ui/theme.js';
20
-
21
- function parseArgs(argv) {
22
- const args = { debug: false, model: null, cwd: process.cwd(), help: false, plan: false, version: false };
23
-
24
- for (let i = 0; i < argv.length; i++) {
25
- const a = argv[i];
26
- if (a === '--debug') args.debug = true;
27
- else if (a === '--plan') args.plan = true;
28
- else if (a === '--model' || a === '-m') args.model = argv[++i];
29
- else if (a === '--cwd' || a === '-C') args.cwd = path.resolve(argv[++i]);
30
- else if (a === '--help' || a === '-h') args.help = true;
31
- else if (a === '--version' || a === '-v') args.version = true;
32
- }
33
-
34
- return args;
35
- }
36
-
37
- function usage() {
38
- const entries = Object.entries(MODELS);
39
- const width = Math.max(...entries.map(([, m]) => m.name.length));
40
- const models = entries
41
- .map(([id, m]) => ` ${m.name.padEnd(width)} ${dim(id)}`)
42
- .join('\n');
43
-
44
- process.stdout.write(
45
- `\n ${blue('ucode')} — a terminal coding agent\n\n` +
46
- ' ucode [options]\n\n' +
47
- ` -m, --model <id> which model to use (default: ${modelName(DEFAULT_MODEL)})\n` +
48
- ' -C, --cwd <dir> work in another directory\n' +
49
- ' --plan start in plan mode: read and research, change nothing\n' +
50
- ' --debug print stack traces when something breaks\n' +
51
- ' -v, --version print the version\n' +
52
- ' -h, --help this message\n' +
53
- ' doctor check that everything ucode needs is working\n\n' +
54
- ` ${sky('Models')}\n${models}\n\n` +
55
- ` Needs UCODE_API_KEY in the environment or in ${ENV_FILE}\n` +
56
- ' Free keys: https://openrouter.ai/keys\n\n'
57
- );
58
- }
59
-
60
- async function main() {
61
- const args = parseArgs(process.argv.slice(2));
62
-
63
- if (args.help) return usage();
64
-
65
- if (process.argv[2] === 'doctor') {
66
- const { runDoctor } = await import('./src/core/doctor.js');
67
- process.stdout.write(`${(await runDoctor()).join('\n')}\n`);
68
- return;
69
- }
70
-
71
- if (args.version) {
72
- process.stdout.write(`${VERSION}\n`);
73
- return;
74
- }
75
-
76
- if (args.model) {
77
- try {
78
- setModel(args.model);
79
- } catch (err) {
80
- new Plain({ cwd: args.cwd }).error(err);
81
- process.exitCode = 1;
82
- return;
83
- }
84
- }
85
-
86
- const agent = new Agent({ cwd: args.cwd, debug: args.debug });
87
- if (args.plan) agent.ui.mode = 'plan';
88
-
89
- try {
90
- await agent.start();
91
- } catch (err) {
92
- agent.ui.error(err, { debug: args.debug });
93
- await agent.persist().catch(() => {});
94
- agent.ui.close();
95
- process.exitCode = 1;
96
- }
97
- }
98
-
99
- /**
100
- * Was this file launched, or imported?
101
- *
102
- * Importing it — which another front end would do to reuse Agent — must not
103
- * open a terminal session. The obvious check is comparing `import.meta.url`
104
- * with argv[1], and it is wrong: after `npm link`, argv[1] arrives as the path
105
- * through the symlink in the global node_modules while `import.meta.url` has
106
- * already been resolved to the real file. The two never match, so the command
107
- * starts, matches nothing, and exits successfully having done absolutely
108
- * nothing which is a great deal harder to diagnose than a crash.
109
- *
110
- * Comparing real paths is what actually answers the question.
111
- */
112
- function launchedDirectly() {
113
- const entry = process.argv[1];
114
- if (!entry) return false;
115
-
116
- const self = fileURLToPath(import.meta.url);
117
- try {
118
- return realpathSync(entry) === realpathSync(self);
119
- } catch {
120
- return pathToFileURL(entry).href === import.meta.url;
121
- }
122
- }
123
-
124
- if (launchedDirectly()) main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ucode.js — the command.
4
+ *
5
+ * Parses the arguments, builds an Agent, and gets out of the way. Everything
6
+ * of substance is under src/.
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { readFile } from 'node:fs/promises';
12
+ import { realpathSync } from 'node:fs';
13
+ import { pathToFileURL, fileURLToPath } from 'node:url';
14
+
15
+ import { Agent } from './src/core/loop.js';
16
+ import { setModel, modelName, MODELS, DEFAULT_MODEL, ENV_FILE } from './src/core/provider.js';
17
+ import { VERSION } from './src/core/version.js';
18
+ import { Plain } from './src/ui/plain.js';
19
+ import { blue, dim, sky } from './src/ui/theme.js';
20
+
21
+ function parseArgs(argv) {
22
+ const args = { debug: false, model: null, cwd: process.cwd(), help: false, plan: false, version: false };
23
+
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--debug') args.debug = true;
27
+ else if (a === '--plan') args.plan = true;
28
+ else if (a === '--model' || a === '-m') args.model = argv[++i];
29
+ else if (a === '--cwd' || a === '-C') args.cwd = path.resolve(argv[++i]);
30
+ else if (a === '--help' || a === '-h') args.help = true;
31
+ else if (a === '--version' || a === '-v') args.version = true;
32
+ }
33
+
34
+ return args;
35
+ }
36
+
37
+ function usage() {
38
+ const entries = Object.entries(MODELS);
39
+ const width = Math.max(...entries.map(([, m]) => m.name.length));
40
+ const models = entries
41
+ .map(([id, m]) => ` ${m.name.padEnd(width)} ${dim(id)}`)
42
+ .join('\n');
43
+
44
+ process.stdout.write(
45
+ `\n ${blue('ucode')} — a terminal coding agent\n\n` +
46
+ ' ucode [options]\n\n' +
47
+ ` -m, --model <id> which model to use (default: ${modelName(DEFAULT_MODEL)})\n` +
48
+ ' -C, --cwd <dir> work in another directory\n' +
49
+ ' --plan start in plan mode: read and research, change nothing\n' +
50
+ ' --debug print stack traces when something breaks\n' +
51
+ ' -v, --version print the version\n' +
52
+ ' -h, --help this message\n' +
53
+ ' doctor check that everything ucode needs is working\n' +
54
+ ' login <key> save your key for every folder on this machine\n\n' +
55
+ ` ${sky('Models')}\n${models}\n\n` +
56
+ ` Needs UCODE_API_KEY in the environment or in ${ENV_FILE}\n` +
57
+ ' Free keys: https://openrouter.ai/keys\n\n'
58
+ );
59
+ }
60
+
61
+ async function main() {
62
+ const args = parseArgs(process.argv.slice(2));
63
+
64
+ if (args.help) return usage();
65
+
66
+ if (process.argv[2] === 'login') {
67
+ const { saveKey } = await import('./src/core/login.js');
68
+ process.stdout.write(`${await saveKey(process.argv[3])}
69
+ `);
70
+ return;
71
+ }
72
+
73
+ if (process.argv[2] === 'doctor') {
74
+ const { runDoctor } = await import('./src/core/doctor.js');
75
+ process.stdout.write(`${(await runDoctor()).join('\n')}\n`);
76
+ return;
77
+ }
78
+
79
+ if (args.version) {
80
+ process.stdout.write(`${VERSION}\n`);
81
+ return;
82
+ }
83
+
84
+ if (args.model) {
85
+ try {
86
+ setModel(args.model);
87
+ } catch (err) {
88
+ new Plain({ cwd: args.cwd }).error(err);
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ }
93
+
94
+ const agent = new Agent({ cwd: args.cwd, debug: args.debug });
95
+ if (args.plan) agent.ui.mode = 'plan';
96
+
97
+ try {
98
+ await agent.start();
99
+ } catch (err) {
100
+ agent.ui.error(err, { debug: args.debug });
101
+ await agent.persist().catch(() => {});
102
+ agent.ui.close();
103
+ process.exitCode = 1;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Was this file launched, or imported?
109
+ *
110
+ * Importing it which another front end would do to reuse Agent — must not
111
+ * open a terminal session. The obvious check is comparing `import.meta.url`
112
+ * with argv[1], and it is wrong: after `npm link`, argv[1] arrives as the path
113
+ * through the symlink in the global node_modules while `import.meta.url` has
114
+ * already been resolved to the real file. The two never match, so the command
115
+ * starts, matches nothing, and exits successfully having done absolutely
116
+ * nothing — which is a great deal harder to diagnose than a crash.
117
+ *
118
+ * Comparing real paths is what actually answers the question.
119
+ */
120
+ function launchedDirectly() {
121
+ const entry = process.argv[1];
122
+ if (!entry) return false;
123
+
124
+ const self = fileURLToPath(import.meta.url);
125
+ try {
126
+ return realpathSync(entry) === realpathSync(self);
127
+ } catch {
128
+ return pathToFileURL(entry).href === import.meta.url;
129
+ }
130
+ }
131
+
132
+ if (launchedDirectly()) main();