@lerret/cli 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/LICENSE +21 -0
- package/dist-studio/.bundle-stamp +34 -0
- package/dist-studio/assets/asset-runtime-MFjDKvQD.js +129 -0
- package/dist-studio/assets/cli-project-source-9dNA_gVa.js +1 -0
- package/dist-studio/assets/dev-harness-BH6a8T7l.js +18 -0
- package/dist-studio/assets/hosted-project-source-dVGq_8c6.js +135 -0
- package/dist-studio/assets/index-BNmJ8c2t.css +1 -0
- package/dist-studio/assets/index-EslqdOhg.js +10 -0
- package/dist-studio/assets/leaf-marker-command.png +0 -0
- package/dist-studio/assets/leaf-marker-comment-box.png +0 -0
- package/dist-studio/assets/leaf-marker-homescreen.png +0 -0
- package/dist-studio/assets/leafmarker-icon-dark-128.png +0 -0
- package/dist-studio/assets/leafmarker-logo-transparent.png +0 -0
- package/dist-studio/assets/leafmarker-logo.png +0 -0
- package/dist-studio/assets/lerret-logo.png +0 -0
- package/dist-studio/assets/lerret-wordmark.svg +3 -0
- package/dist-studio/assets/logo-angular.svg +1 -0
- package/dist-studio/assets/logo-claude.svg +7 -0
- package/dist-studio/assets/logo-codex.svg +1 -0
- package/dist-studio/assets/logo-cursor.svg +1 -0
- package/dist-studio/assets/logo-javascript.svg +1 -0
- package/dist-studio/assets/logo-react.svg +1 -0
- package/dist-studio/assets/logo-svelte.svg +1 -0
- package/dist-studio/assets/logo-vue.svg +1 -0
- package/dist-studio/assets/open-folder-D5OR7eLb.js +8 -0
- package/dist-studio/assets/project-studio-BjNaIuRb.js +795 -0
- package/dist-studio/assets/project-studio-CKuMOMsC.css +1 -0
- package/dist-studio/assets/superwhisper-logo.png +0 -0
- package/dist-studio/index.html +47 -0
- package/dist-studio/module-sw.js +275 -0
- package/package.json +51 -0
- package/src/dev.js +373 -0
- package/src/export.js +1386 -0
- package/src/fs/node-backend.js +631 -0
- package/src/lerret.js +143 -0
- package/src/resolve-project.js +178 -0
- package/src/vite-plugin-lerret-project.js +986 -0
- package/src/watcher.js +214 -0
package/src/dev.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// `lerret dev` — run the studio against a project folder.
|
|
2
|
+
//
|
|
3
|
+
// Boots a Node-side Vite dev server that serves the bundled studio plus the
|
|
4
|
+
// user's `.lerret/` folder, opens the studio in the browser (per `--open`),
|
|
5
|
+
// and runs until the user kills the process. Argument parsing uses node's
|
|
6
|
+
// built-in `util.parseArgs` — the architecture's explicit choice, no heavy
|
|
7
|
+
// CLI framework.
|
|
8
|
+
//
|
|
9
|
+
// ── Flags (PRD contract — names are fixed) ─────────────────────────────────
|
|
10
|
+
// --port <n> Dev-server port. Defaults to Vite's default.
|
|
11
|
+
// --folder <path> Override the project folder, bypassing walk-up auto-
|
|
12
|
+
// detection. Useful for "I'm not cd'd into
|
|
13
|
+
// the project, but I want to point at THIS folder".
|
|
14
|
+
// --open Open the studio in the browser on start (Vite's
|
|
15
|
+
// `server.open`). Flip off with `--no-open`. Default: on.
|
|
16
|
+
// --help, -h Print this command's usage and exit.
|
|
17
|
+
//
|
|
18
|
+
// ── How the studio is loaded ───────────────────────────────────────────────
|
|
19
|
+
// We point Vite at the **studio package's source directory** — i.e. the same
|
|
20
|
+
// dir its standalone `vite dev` runs against. Vite serves `index.html` +
|
|
21
|
+
// `src/main.jsx` from there. The Lerret-specific bits are added by
|
|
22
|
+
// `vite-plugin-lerret-project`: a virtual module exposing the scanned project
|
|
23
|
+
// model, a stable URL prefix aliased to the user's project root, and the
|
|
24
|
+
// chokidar-driven `lerret:change` HMR event.
|
|
25
|
+
//
|
|
26
|
+
// In dev mode the studio is loaded *from source*. The production path
|
|
27
|
+
// uses a pre-built static bundle shipped inside the `lerret` npm package;
|
|
28
|
+
// the plugin contract here is the same, only the bit of code that figures
|
|
29
|
+
// out where the studio's HTML + JS live differs.
|
|
30
|
+
|
|
31
|
+
import { parseArgs } from 'node:util';
|
|
32
|
+
import { dirname, resolve } from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
import { realpathOrSelf, pathExists } from './fs/node-backend.js';
|
|
36
|
+
import { resolveProject } from './resolve-project.js';
|
|
37
|
+
import {
|
|
38
|
+
lerretProjectPlugin,
|
|
39
|
+
normalizeFolderArg,
|
|
40
|
+
PROJECT_ASSET_BASE_URL,
|
|
41
|
+
} from './vite-plugin-lerret-project.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The argv shape `parseArgs` produces for `lerret dev`.
|
|
45
|
+
*
|
|
46
|
+
* @typedef {object} DevFlags
|
|
47
|
+
* @property {number | undefined} port
|
|
48
|
+
* @property {string | undefined} folder
|
|
49
|
+
* @property {boolean} open
|
|
50
|
+
* @property {boolean} help
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Print the `dev`-subcommand-specific usage banner.
|
|
55
|
+
*
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function printUsage() {
|
|
59
|
+
const lines = [
|
|
60
|
+
'lerret dev — run the studio against a project folder.',
|
|
61
|
+
'',
|
|
62
|
+
'Usage: lerret dev [options]',
|
|
63
|
+
'',
|
|
64
|
+
'Options:',
|
|
65
|
+
' --port <n> Dev-server port (default: Vite default)',
|
|
66
|
+
' --folder <path> Project folder (bypasses walk-up auto-detection)',
|
|
67
|
+
' --open Open the studio in the browser on start (default)',
|
|
68
|
+
' --no-open Do not open the browser on start',
|
|
69
|
+
' -h, --help Show this help',
|
|
70
|
+
];
|
|
71
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse `lerret dev`'s argv. A separate function so tests can verify flag
|
|
76
|
+
* handling without spinning up a server.
|
|
77
|
+
*
|
|
78
|
+
* @param {string[]} argv
|
|
79
|
+
* @returns {{ flags: DevFlags, error: string | null }}
|
|
80
|
+
* `error` is set when parsing fails — the caller prints it and the usage
|
|
81
|
+
* banner. On success the caller acts on `flags`.
|
|
82
|
+
*/
|
|
83
|
+
export function parseDevArgs(argv) {
|
|
84
|
+
// `--no-open` is the documented PRD spelling for "do not open the
|
|
85
|
+
// browser". Node's `parseArgs` does not turn `--no-<name>` into a boolean
|
|
86
|
+
// false automatically in `strict` mode — it rejects the unknown flag — so
|
|
87
|
+
// we strip the token here and remember the intent for the result. This
|
|
88
|
+
// keeps the strict-unknown-flag check working for every OTHER bogus flag.
|
|
89
|
+
let openIntent;
|
|
90
|
+
const filteredArgv = [];
|
|
91
|
+
for (const tok of argv) {
|
|
92
|
+
if (tok === '--no-open') {
|
|
93
|
+
openIntent = false;
|
|
94
|
+
} else {
|
|
95
|
+
filteredArgv.push(tok);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseArgs({
|
|
102
|
+
args: filteredArgv,
|
|
103
|
+
options: {
|
|
104
|
+
port: { type: 'string' },
|
|
105
|
+
folder: { type: 'string' },
|
|
106
|
+
open: { type: 'boolean' },
|
|
107
|
+
help: { type: 'boolean', short: 'h' },
|
|
108
|
+
},
|
|
109
|
+
// Reject unknown flags rather than silently ignoring them — surface a
|
|
110
|
+
// typo as a usage error.
|
|
111
|
+
strict: true,
|
|
112
|
+
// No positionals are expected; reject them too.
|
|
113
|
+
allowPositionals: false,
|
|
114
|
+
});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { flags: /** @type {any} */ (null), error: err && err.message ? err.message : String(err) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const values = parsed.values || {};
|
|
120
|
+
|
|
121
|
+
// Port: `parseArgs` returns a string; coerce here. An invalid port is a
|
|
122
|
+
// usage error.
|
|
123
|
+
let port;
|
|
124
|
+
if (typeof values.port === 'string') {
|
|
125
|
+
const n = Number(values.port);
|
|
126
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
127
|
+
return { flags: /** @type {any} */ (null), error: `--port: not a valid port number: ${values.port}` };
|
|
128
|
+
}
|
|
129
|
+
port = n;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Folder: pass through as a string; resolution to an absolute path is the
|
|
133
|
+
// caller's job (so tests can verify the resolution step separately).
|
|
134
|
+
const folder = typeof values.folder === 'string' ? values.folder : undefined;
|
|
135
|
+
|
|
136
|
+
// Open: default true (matches "open the studio in the browser" in the
|
|
137
|
+
// PRD). Explicit `--open` overrides nothing (it sets true already);
|
|
138
|
+
// `--no-open` (stripped above) overrides to false.
|
|
139
|
+
let open;
|
|
140
|
+
if (openIntent === false) {
|
|
141
|
+
open = false;
|
|
142
|
+
} else if (values.open === true) {
|
|
143
|
+
open = true;
|
|
144
|
+
} else {
|
|
145
|
+
open = true; // default
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
flags: {
|
|
150
|
+
port,
|
|
151
|
+
folder,
|
|
152
|
+
open,
|
|
153
|
+
help: !!values.help,
|
|
154
|
+
},
|
|
155
|
+
error: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Locate the studio root that the dev server will serve.
|
|
161
|
+
*
|
|
162
|
+
* Resolution order:
|
|
163
|
+
* 1. `<cli-package>/dist-studio/` — the pre-built static assets bundled
|
|
164
|
+
* into the published `lerret` package. Present after `pnpm --filter
|
|
165
|
+
* @lerret/cli build` (or after `npm install lerret`).
|
|
166
|
+
* 2. `<monorepo>/packages/studio/` source — the workspace fallback for
|
|
167
|
+
* fresh checkouts or contributors who haven't run the build yet. Vite
|
|
168
|
+
* will serve from source in this path, so HMR works but the bundle is
|
|
169
|
+
* NOT production-optimised. A warning is printed so the developer knows.
|
|
170
|
+
*
|
|
171
|
+
* In the published npm tarball only `dist-studio/` exists (the source is not
|
|
172
|
+
* shipped), so path 1 is the only option for end users. Path 2 is a dev
|
|
173
|
+
* convenience — it keeps `lerret dev` usable in the monorepo even without
|
|
174
|
+
* a preceding build step.
|
|
175
|
+
*
|
|
176
|
+
* @returns {string} An absolute path to the studio root (static or source).
|
|
177
|
+
*/
|
|
178
|
+
export function resolveStudioRoot() {
|
|
179
|
+
// This file: packages/cli/src/dev.js → packages/cli/src/ → packages/cli/
|
|
180
|
+
const cliDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
181
|
+
|
|
182
|
+
// 1. Prefer the pre-built dist-studio/ (production / published case).
|
|
183
|
+
const distStudio = resolve(cliDir, 'dist-studio');
|
|
184
|
+
if (pathExists(resolve(distStudio, 'index.html'))) {
|
|
185
|
+
return distStudio;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2. Fall back to the studio source for in-monorepo dev without a build.
|
|
189
|
+
// The warning surfaces the situation so contributors know why startup
|
|
190
|
+
// is slower (Vite transforms source on every request).
|
|
191
|
+
const studioSource = resolve(cliDir, '..', 'studio');
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
'lerret: dist-studio/ not found — serving studio from source.\n' +
|
|
194
|
+
' Run `pnpm --filter @lerret/cli build` for production performance.\n',
|
|
195
|
+
);
|
|
196
|
+
return studioSource;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run `lerret dev`. Resolves the project, starts the Vite server, waits for
|
|
201
|
+
* Ctrl-C, and returns an exit code.
|
|
202
|
+
*
|
|
203
|
+
* @param {string[]} argv Argv slice after the `dev` subcommand.
|
|
204
|
+
* @returns {Promise<number>} Exit code. 0 on graceful shutdown, 1 on an
|
|
205
|
+
* unrecoverable error (Vite failed to start, port already in use, etc.).
|
|
206
|
+
*/
|
|
207
|
+
export async function runDev(argv) {
|
|
208
|
+
const { flags, error } = parseDevArgs(argv);
|
|
209
|
+
if (error) {
|
|
210
|
+
process.stderr.write(`lerret dev: ${error}\n\n`);
|
|
211
|
+
printUsage();
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (flags.help) {
|
|
216
|
+
printUsage();
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 1. Resolve the project.
|
|
221
|
+
//
|
|
222
|
+
// The PRD's flow:
|
|
223
|
+
// - `--folder <path>` overrides the start dir for the walk-up.
|
|
224
|
+
// - Otherwise we start from `process.cwd()` and let `resolveProject`
|
|
225
|
+
// find the nearest ancestor that owns `.lerret/`.
|
|
226
|
+
//
|
|
227
|
+
// A NOT-FOUND result is NOT a crash (FR43). We still start
|
|
228
|
+
// the dev server, but the plugin exposes `project: null` and the studio
|
|
229
|
+
// mounts its no-folder placeholder. This makes `lerret dev` always
|
|
230
|
+
// reachable — even invoked from the wrong directory, the user sees the
|
|
231
|
+
// studio and is guided to open a folder.
|
|
232
|
+
const startDir = flags.folder
|
|
233
|
+
? normalizeFolderArg(flags.folder)
|
|
234
|
+
: process.cwd();
|
|
235
|
+
|
|
236
|
+
const projectResolution = await resolveProject(startDir);
|
|
237
|
+
|
|
238
|
+
// Vite's `server.fs.allow` compares against symlink-resolved paths, so
|
|
239
|
+
// we *always* canonicalize the project root before handing it to the
|
|
240
|
+
// plugin. On macOS `/tmp` → `/private/tmp` is the classic gotcha; the
|
|
241
|
+
// helper is a no-op for already-canonical paths.
|
|
242
|
+
/** @type {string | null} */
|
|
243
|
+
const projectRoot = projectResolution.found
|
|
244
|
+
? realpathOrSelf(projectResolution.projectRoot).replaceAll('\\', '/')
|
|
245
|
+
: null;
|
|
246
|
+
/** @type {string | null} */
|
|
247
|
+
const lerretDir = projectResolution.found
|
|
248
|
+
? realpathOrSelf(projectResolution.lerretDir).replaceAll('\\', '/')
|
|
249
|
+
: null;
|
|
250
|
+
|
|
251
|
+
if (projectResolution.found) {
|
|
252
|
+
process.stdout.write(`lerret dev: project ${projectRoot}\n`);
|
|
253
|
+
} else {
|
|
254
|
+
process.stdout.write(
|
|
255
|
+
`lerret dev: no \`.lerret/\` project found from ${startDir} — starting in no-folder mode.\n`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 2. Find the studio source dir Vite will serve as its root.
|
|
260
|
+
const studioRoot = resolveStudioRoot();
|
|
261
|
+
|
|
262
|
+
// 3. Boot Vite programmatically.
|
|
263
|
+
//
|
|
264
|
+
// We import `vite` dynamically so the CLI's static-analysis (and the
|
|
265
|
+
// `lerret --help` path) doesn't pay the import cost up front.
|
|
266
|
+
//
|
|
267
|
+
// When serving from pre-built `dist-studio/` assets:
|
|
268
|
+
// - `@vitejs/plugin-react` is NOT needed — the JSX is already compiled.
|
|
269
|
+
// - Only the `lerretProjectPlugin` is needed (virtual module + HMR).
|
|
270
|
+
//
|
|
271
|
+
// When serving from studio source (fallback path):
|
|
272
|
+
// - `@vitejs/plugin-react` IS needed for the JSX transform + Fast Refresh.
|
|
273
|
+
//
|
|
274
|
+
// `searchForWorkspaceRoot` finds the workspace root (the pnpm-
|
|
275
|
+
// workspace's top-level), which is what Vite uses by default when
|
|
276
|
+
// `server.fs.allow` is undefined. We set it explicitly so we can append
|
|
277
|
+
// the user's project root without losing the workspace access the studio
|
|
278
|
+
// needs (its node_modules, its source dir, etc.).
|
|
279
|
+
const vite = await import('vite');
|
|
280
|
+
const { createServer, searchForWorkspaceRoot } = vite;
|
|
281
|
+
|
|
282
|
+
// Whether we are serving from the pre-built CLI bundle or from source.
|
|
283
|
+
const cliDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
284
|
+
const isPreBuilt = pathExists(resolve(cliDir, 'dist-studio', 'index.html')) &&
|
|
285
|
+
studioRoot === resolve(cliDir, 'dist-studio');
|
|
286
|
+
|
|
287
|
+
const plugins = [lerretProjectPlugin({ projectRoot, lerretDir })];
|
|
288
|
+
if (!isPreBuilt) {
|
|
289
|
+
// Serving from source — need the React plugin for JSX transform.
|
|
290
|
+
const reactPlugin = (await import('@vitejs/plugin-react')).default;
|
|
291
|
+
plugins.unshift(reactPlugin());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const workspaceRoot = searchForWorkspaceRoot(studioRoot);
|
|
295
|
+
|
|
296
|
+
const server = await createServer({
|
|
297
|
+
// Don't pick up the studio's own `vite.config.js` (it has a fixture
|
|
298
|
+
// alias the CLI doesn't want); we hand Vite a clean inline config.
|
|
299
|
+
configFile: false,
|
|
300
|
+
root: studioRoot,
|
|
301
|
+
plugins,
|
|
302
|
+
server: {
|
|
303
|
+
port: flags.port,
|
|
304
|
+
open: flags.open,
|
|
305
|
+
fs: {
|
|
306
|
+
// Studio root + monorepo/workspace root. The plugin appends the
|
|
307
|
+
// user's project root on top of this list. The CLI never writes
|
|
308
|
+
// to any of these — only reads (NFR13).
|
|
309
|
+
allow: [studioRoot, workspaceRoot],
|
|
310
|
+
},
|
|
311
|
+
// `host` left undefined so Vite uses its default (localhost). Users
|
|
312
|
+
// who need network access can re-run with --port and Vite's own
|
|
313
|
+
// --host knob in a future change; for `lerret dev` against a local
|
|
314
|
+
// user folder the default localhost behavior is right.
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await server.listen();
|
|
319
|
+
server.printUrls();
|
|
320
|
+
|
|
321
|
+
// The asset base URL the studio expects — log so a curious user can see
|
|
322
|
+
// it (and to help debugging if a future change touches the contract).
|
|
323
|
+
if (projectRoot) {
|
|
324
|
+
process.stdout.write(`lerret dev: serving project at ${PROJECT_ASSET_BASE_URL}/\n`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 4. Hold the process open until SIGINT/SIGTERM, then close cleanly.
|
|
328
|
+
//
|
|
329
|
+
// Without this `await`, runDev would resolve and the CLI would exit
|
|
330
|
+
// before the server ever served a request. With it, the function
|
|
331
|
+
// resolves only when the user kills the process — perfect for a
|
|
332
|
+
// long-running dev command.
|
|
333
|
+
return await waitForShutdown(server);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Resolve when the dev server should shut down — on SIGINT (Ctrl-C) or
|
|
338
|
+
* SIGTERM. The handlers are removed after the first signal so a second one
|
|
339
|
+
* can hard-kill if the close hangs.
|
|
340
|
+
*
|
|
341
|
+
* @param {import('vite').ViteDevServer} server
|
|
342
|
+
* @returns {Promise<number>}
|
|
343
|
+
*/
|
|
344
|
+
function waitForShutdown(server) {
|
|
345
|
+
return new Promise((resolve) => {
|
|
346
|
+
let shuttingDown = false;
|
|
347
|
+
const onSignal = async (signal) => {
|
|
348
|
+
if (shuttingDown) return;
|
|
349
|
+
shuttingDown = true;
|
|
350
|
+
process.stdout.write(`\nlerret dev: ${signal} received, shutting down…\n`);
|
|
351
|
+
try {
|
|
352
|
+
await server.close();
|
|
353
|
+
} catch {
|
|
354
|
+
// Best effort; we're exiting anyway.
|
|
355
|
+
}
|
|
356
|
+
// Detach the other handler so a second Ctrl-C exits immediately.
|
|
357
|
+
process.removeListener('SIGINT', onSigint);
|
|
358
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
359
|
+
resolve(0);
|
|
360
|
+
};
|
|
361
|
+
const onSigint = () => onSignal('SIGINT');
|
|
362
|
+
const onSigterm = () => onSignal('SIGTERM');
|
|
363
|
+
process.once('SIGINT', onSigint);
|
|
364
|
+
process.once('SIGTERM', onSigterm);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Re-export of the dev-side asset base URL, so the few studio-side tests
|
|
370
|
+
* that need it can import it from the CLI without depending on the plugin
|
|
371
|
+
* file directly.
|
|
372
|
+
*/
|
|
373
|
+
export { PROJECT_ASSET_BASE_URL };
|