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 +388 -0
- package/package.json +21 -0
- package/template/.agents/skills/.gitkeep +0 -0
- package/template/README.md +23 -0
- package/template/_gitignore +4 -0
- package/template/package.json.template +16 -0
- package/template/src/index.ts +1 -0
- package/template/src/shared/utils.ts +3 -0
- package/template/src/workflows/star-repo.ts +9 -0
- package/template/tsconfig.json +12 -0
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,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,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
|
+
});
|