create-libretto 0.6.2

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/index.mjs ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync, spawn } from "node:child_process";
4
+ import {
5
+ cpSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ renameSync,
11
+ realpathSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { basename, dirname, join, relative, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // ANSI helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const DIM = "\x1b[2m";
25
+ const BOLD = "\x1b[1m";
26
+ const RESET = "\x1b[0m";
27
+ const CLEAR_LINE = "\x1b[2K";
28
+ const RED = "\x1b[31m";
29
+ const GREEN = "\x1b[32m";
30
+ const CYAN = "\x1b[36m";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Package manager detection
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Detect the package manager that invoked `create-libretto` by inspecting the
38
+ * `npm_config_user_agent` env var (Vite-style detection).
39
+ */
40
+ export function detectPackageManager() {
41
+ const ua = process.env.npm_config_user_agent ?? "";
42
+ if (ua.startsWith("pnpm")) return "pnpm";
43
+ if (ua.startsWith("yarn")) return "yarn";
44
+ if (ua.startsWith("bun")) return "bun";
45
+ return "npm";
46
+ }
47
+
48
+ /** Return the exec command for running a local bin with the given package manager. */
49
+ function execCommand(pkgManager) {
50
+ switch (pkgManager) {
51
+ case "pnpm":
52
+ return "pnpm exec";
53
+ case "yarn":
54
+ return "yarn";
55
+ case "bun":
56
+ return "bunx";
57
+ default:
58
+ return "npx";
59
+ }
60
+ }
61
+
62
+ /** Return the install command for the given package manager. */
63
+ function installCommand(pkgManager) {
64
+ switch (pkgManager) {
65
+ case "yarn":
66
+ return "yarn";
67
+ case "bun":
68
+ return "bun install";
69
+ default:
70
+ return `${pkgManager} install`;
71
+ }
72
+ }
73
+
74
+ /** Return the run command for scripts (used in next-steps messaging). */
75
+ function runCommand(pkgManager) {
76
+ switch (pkgManager) {
77
+ case "npm":
78
+ return "npx";
79
+ case "pnpm":
80
+ return "pnpm exec";
81
+ case "yarn":
82
+ return "yarn";
83
+ case "bun":
84
+ return "bunx";
85
+ default:
86
+ return "npx";
87
+ }
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Interactive prompt
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Interactive prompt with a dim placeholder that disappears while typing
96
+ * and reappears when the input is empty, like create-next-app.
97
+ * Shows ✔ in green on completion.
98
+ */
99
+ function promptProjectName(defaultName) {
100
+ return new Promise((resolve) => {
101
+ const label = `${BOLD}What is your project named?${RESET}`;
102
+ const pendingPrompt = `${CYAN}?${RESET} ${label} `;
103
+ const donePrompt = `${GREEN}✔${RESET} ${label} `;
104
+ let value = "";
105
+
106
+ function render() {
107
+ const display = value || `${DIM}${defaultName}${RESET}`;
108
+ process.stdout.write(`\r${CLEAR_LINE}${pendingPrompt}${display}`);
109
+ // Place cursor right after the typed text (not after the placeholder)
110
+ if (!value) {
111
+ const placeholderLen = defaultName.length;
112
+ process.stdout.write(`\x1b[${placeholderLen}D`);
113
+ }
114
+ }
115
+
116
+ process.stdin.setRawMode(true);
117
+ process.stdin.resume();
118
+ process.stdin.setEncoding("utf-8");
119
+
120
+ render();
121
+
122
+ process.stdin.on("data", (key) => {
123
+ // Ctrl+C
124
+ if (key === "\x03") {
125
+ process.stdin.setRawMode(false);
126
+ process.stdin.pause();
127
+ process.stdout.write(`\n${RED}Cancelled${RESET}\n`);
128
+ process.exit(130);
129
+ }
130
+ // Enter
131
+ if (key === "\r" || key === "\n") {
132
+ process.stdin.setRawMode(false);
133
+ process.stdin.pause();
134
+ process.stdin.removeAllListeners("data");
135
+ const resolved = value || defaultName;
136
+ process.stdout.write(`\r${CLEAR_LINE}${donePrompt}${resolved}\n`);
137
+ resolve(resolved);
138
+ return;
139
+ }
140
+ // Backspace / Delete
141
+ if (key === "\x7f" || key === "\b") {
142
+ value = value.slice(0, -1);
143
+ render();
144
+ return;
145
+ }
146
+ // Ignore other control characters
147
+ if (key.charCodeAt(0) < 32) return;
148
+ value += key;
149
+ render();
150
+ });
151
+ });
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Spinner
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
159
+
160
+ function createSpinner(message) {
161
+ let i = 0;
162
+ const interval = setInterval(() => {
163
+ const frame = SPINNER_FRAMES[i++ % SPINNER_FRAMES.length];
164
+ process.stdout.write(`\r${CLEAR_LINE}${frame} ${message}`);
165
+ }, 80);
166
+ return {
167
+ stop(finalMessage) {
168
+ clearInterval(interval);
169
+ process.stdout.write(`\r${CLEAR_LINE}${finalMessage ?? ""}\n`);
170
+ },
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Run install command asynchronously so the spinner can animate.
176
+ * Returns { stdout, stderr, status }.
177
+ */
178
+ function runInstallAsync(cmd, cwd) {
179
+ return new Promise((resolve) => {
180
+ const [bin, ...args] = cmd.split(" ");
181
+ const child = spawn(bin, args, {
182
+ cwd,
183
+ stdio: ["inherit", "pipe", "pipe"],
184
+ shell: true,
185
+ });
186
+ let stdout = "";
187
+ let stderr = "";
188
+ child.stdout.on("data", (data) => {
189
+ stdout += data;
190
+ });
191
+ child.stderr.on("data", (data) => {
192
+ stderr += data;
193
+ });
194
+ child.on("close", (status, signal) => {
195
+ resolve({ stdout, stderr, status, signal });
196
+ });
197
+ });
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Scaffold
202
+ // ---------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Read dependencies and devDependencies from the generated package.json.
206
+ */
207
+ function readDepsFromPackageJson(targetDir) {
208
+ const pkg = JSON.parse(
209
+ readFileSync(join(targetDir, "package.json"), "utf-8"),
210
+ );
211
+ return {
212
+ dependencies: Object.keys(pkg.dependencies ?? {}),
213
+ devDependencies: Object.keys(pkg.devDependencies ?? {}),
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Scaffold a new Libretto project into `targetDir`.
219
+ *
220
+ * Exported so tests can call it directly with `skipInstall: true`.
221
+ */
222
+ export async function scaffoldProject(
223
+ targetDir,
224
+ projectName,
225
+ pkgManager,
226
+ { skipInstall = false } = {},
227
+ ) {
228
+ const templateDir = join(__dirname, "template");
229
+
230
+ // 1. Copy template/ → targetDir (recursive)
231
+ mkdirSync(targetDir, { recursive: true });
232
+ cpSync(templateDir, targetDir, { recursive: true });
233
+
234
+ // 2. Rename _gitignore → .gitignore
235
+ const gitignoreSrc = join(targetDir, "_gitignore");
236
+ if (existsSync(gitignoreSrc)) {
237
+ renameSync(gitignoreSrc, join(targetDir, ".gitignore"));
238
+ }
239
+
240
+ // 3. Process package.json.template → package.json
241
+ // Set LIBRETTO_DEV=1 to use a file: dependency pointing at the local build.
242
+ const localLibrettoDir = resolve(__dirname, "..", "libretto");
243
+ let librettoVersion;
244
+ if (process.env.LIBRETTO_DEV === "1") {
245
+ librettoVersion = `file:${localLibrettoDir}`;
246
+ } else {
247
+ const ownPkg = JSON.parse(
248
+ readFileSync(join(__dirname, "package.json"), "utf-8"),
249
+ );
250
+ librettoVersion = `^${ownPkg.version}`;
251
+ }
252
+
253
+ const pkgTemplatePath = join(targetDir, "package.json.template");
254
+ const pkgContents = readFileSync(pkgTemplatePath, "utf-8")
255
+ .replaceAll("{{projectName}}", projectName)
256
+ .replaceAll("{{librettoVersion}}", librettoVersion);
257
+ writeFileSync(join(targetDir, "package.json"), pkgContents);
258
+ unlinkSync(pkgTemplatePath);
259
+
260
+ // 4. Process README.md
261
+ const readmePath = join(targetDir, "README.md");
262
+ const readmeContents = readFileSync(readmePath, "utf-8")
263
+ .replaceAll("{{projectName}}", projectName)
264
+ .replaceAll("{{runCommand}}", runCommand(pkgManager));
265
+ writeFileSync(readmePath, readmeContents);
266
+
267
+ // 5. Install dependencies & run setup
268
+ if (!skipInstall) {
269
+ const { dependencies, devDependencies } =
270
+ readDepsFromPackageJson(targetDir);
271
+
272
+ if (dependencies.length > 0) {
273
+ console.log(`Installing dependencies:`);
274
+ for (const dep of dependencies) {
275
+ console.log(`- ${dep}`);
276
+ }
277
+ if (devDependencies.length > 0) console.log();
278
+ }
279
+
280
+ if (devDependencies.length > 0) {
281
+ console.log(`Installing devDependencies:`);
282
+ for (const dep of devDependencies) {
283
+ console.log(`- ${dep}`);
284
+ }
285
+ }
286
+ console.log();
287
+
288
+ const spinner = createSpinner("Installing packages...");
289
+ const result = await runInstallAsync(installCommand(pkgManager), targetDir);
290
+ spinner.stop();
291
+
292
+ // Print stderr (warnings)
293
+ if (result.stderr) {
294
+ for (const line of result.stderr.split("\n")) {
295
+ if (line.trim()) console.error(line);
296
+ }
297
+ }
298
+
299
+ if (result.status !== 0) {
300
+ console.error(`\nFailed to install dependencies.`);
301
+ process.exit(1);
302
+ }
303
+
304
+ // Print stdout summary lines (e.g. "added 123 packages...")
305
+ if (result.stdout) {
306
+ for (const line of result.stdout.split("\n")) {
307
+ const trimmed = line.trim();
308
+ if (trimmed) console.log(trimmed);
309
+ }
310
+ }
311
+
312
+ console.log();
313
+
314
+ try {
315
+ execSync(`${execCommand(pkgManager)} libretto setup`, {
316
+ cwd: targetDir,
317
+ stdio: "inherit",
318
+ });
319
+ } catch {
320
+ console.error(`\nFailed to run libretto setup.`);
321
+ process.exit(1);
322
+ }
323
+ }
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Main
328
+ // ---------------------------------------------------------------------------
329
+
330
+ async function main() {
331
+ process.on("SIGINT", () => {
332
+ console.log(`\n${RED}Cancelled${RESET}`);
333
+ process.exit(130);
334
+ });
335
+
336
+ if (process.env.LIBRETTO_DEV === "1") {
337
+ const localLibrettoDir = resolve(__dirname, "..", "libretto");
338
+ console.log(`${DIM}Dev mode: using local libretto from ${localLibrettoDir}${RESET}\n`);
339
+ }
340
+
341
+ const DEFAULT_NAME = "my-automations";
342
+ let rawName = process.argv[2];
343
+
344
+ if (!rawName) {
345
+ if (process.stdin.isTTY) {
346
+ rawName = await promptProjectName(DEFAULT_NAME);
347
+ } else {
348
+ rawName = DEFAULT_NAME;
349
+ }
350
+ }
351
+
352
+ const targetDir = resolve(rawName);
353
+ const projectName = basename(targetDir);
354
+ const pkgManager = detectPackageManager();
355
+
356
+ // Bail if directory exists and is non-empty
357
+ if (existsSync(targetDir)) {
358
+ const entries = readdirSync(targetDir);
359
+ if (entries.length > 0) {
360
+ console.error(
361
+ `Error: Target directory "${targetDir}" already exists and is not empty.`,
362
+ );
363
+ process.exit(1);
364
+ }
365
+ }
366
+
367
+ console.log(
368
+ `\nCreating a new Libretto project in ${BOLD}${targetDir}${RESET}.\n`,
369
+ );
370
+ console.log(`Using ${BOLD}${pkgManager}${RESET} and TypeScript.\n`);
371
+
372
+ await scaffoldProject(targetDir, projectName, pkgManager);
373
+
374
+ console.log(
375
+ `\n${GREEN}Success!${RESET} Created ${BOLD}${projectName}${RESET} at ${targetDir}\n`,
376
+ );
377
+ }
378
+
379
+ // Only run main when this file is executed directly (not imported)
380
+ if (
381
+ process.argv[1] &&
382
+ realpathSync(resolve(process.argv[1])) === fileURLToPath(import.meta.url)
383
+ ) {
384
+ main().catch((err) => {
385
+ console.error(err);
386
+ process.exit(1);
387
+ });
388
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "create-libretto",
3
+ "version": "0.6.2",
4
+ "description": "Create and set up a Libretto project",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/saffron-health/libretto"
9
+ },
10
+ "type": "module",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "bin": {
15
+ "create-libretto": "./index.mjs"
16
+ },
17
+ "files": [
18
+ "index.mjs",
19
+ "template/**"
20
+ ]
21
+ }
File without changes
@@ -0,0 +1,23 @@
1
+ # {{projectName}}
2
+
3
+ Browser automations built with [Libretto](https://libretto.sh).
4
+
5
+ ## Development
6
+
7
+ Start exploring a page interactively:
8
+
9
+ ```bash
10
+ {{runCommand}} libretto open https://example.com --headed
11
+ ```
12
+
13
+ Run a workflow:
14
+
15
+ ```bash
16
+ {{runCommand}} libretto run src/workflows/star-repo.ts
17
+ ```
18
+
19
+ ## Learn more
20
+
21
+ - [Libretto docs](https://libretto.sh)
22
+ - [CLI reference](https://libretto.sh/cli-reference/open-and-connect)
23
+ - [Workflow API](https://libretto.sh/library-api/workflow)
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ .libretto/sessions/
3
+ .libretto/profiles/
4
+ dist/
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "libretto open --headed",
8
+ "build": "tsc"
9
+ },
10
+ "dependencies": {
11
+ "libretto": "{{librettoVersion}}"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^5.8.0"
15
+ }
16
+ }
@@ -0,0 +1 @@
1
+ export { starRepo } from "./workflows/star-repo.js";
@@ -0,0 +1,3 @@
1
+ export function log(message: string): void {
2
+ console.log(`[libretto] ${message}`);
3
+ }
@@ -0,0 +1,9 @@
1
+ import { workflow } from "libretto";
2
+ import { log } from "../shared/utils.js";
3
+
4
+ export const starRepo = workflow("star-repo", async ({ page }) => {
5
+ log("Navigating to Libretto repo...");
6
+ await page.goto("https://github.com/saffron-health/libretto");
7
+ await page.locator('button:has-text("Star")').click();
8
+ log("Starred the repo!");
9
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["src/**/*.ts"]
12
+ }