libretto 0.6.5 → 0.6.6

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.
@@ -3,6 +3,7 @@ import { writeFile } from "node:fs/promises";
3
3
  import { cwd } from "node:process";
4
4
  import { isAbsolute, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { loadProjectEnv } from "../../shared/env/load-env.js";
6
7
  import {
7
8
  getDefaultWorkflowFromModuleExports,
8
9
  getWorkflowsFromModuleExports,
@@ -126,6 +127,10 @@ async function installHeadedWorkflowVisualization(args) {
126
127
  async function runIntegrationInternal(args, options) {
127
128
  const { logger } = options;
128
129
  const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
130
+ const envPath = loadProjectEnv(absolutePath);
131
+ if (envPath) {
132
+ logger.info("loaded-env", { path: envPath });
133
+ }
129
134
  const workflow = await loadDefaultWorkflow(absolutePath);
130
135
  const signalPaths = getPauseSignalPaths(args.session);
131
136
  await removeSignalIfExists(signalPaths.pausedSignalPath);
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { attemptWithRecovery } from './runtime/recovery/recovery.js';
6
6
  export { DetectedSubmissionError, KnownSubmissionError, detectSubmissionError } from './runtime/recovery/errors.js';
7
7
  export { ExtractOptions, extractFromPage } from './runtime/extract/extract.js';
8
8
  export { PageRequestOptions, RequestConfig, pageRequest } from './runtime/network/network.js';
9
- export { DownloadResult, DownloadViaClickOptions, SaveDownloadOptions, downloadAndSave, downloadViaClick } from './runtime/download/download.js';
9
+ export { DownloadResult, DownloadViaClickOptions, downloadViaClick } from './runtime/download/download.js';
10
10
  export { pause } from './shared/debug/pause.js';
11
11
  export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instrumentContext, instrumentPage } from './shared/instrumentation/instrument.js';
12
12
  export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
package/dist/index.js CHANGED
@@ -29,8 +29,7 @@ import {
29
29
  pageRequest
30
30
  } from "./runtime/network/network.js";
31
31
  import {
32
- downloadViaClick,
33
- downloadAndSave
32
+ downloadViaClick
34
33
  } from "./runtime/download/download.js";
35
34
  import { pause } from "./shared/debug/pause.js";
36
35
  import {
@@ -88,7 +87,6 @@ export {
88
87
  createFileLogSink,
89
88
  defaultLogger,
90
89
  detectSubmissionError,
91
- downloadAndSave,
92
90
  downloadViaClick,
93
91
  ensureGhostCursor,
94
92
  ensureHighlightLayer,
@@ -20,16 +20,5 @@ type DownloadViaClickOptions = {
20
20
  * never missed.
21
21
  */
22
22
  declare function downloadViaClick(page: Page, selector: string, options?: DownloadViaClickOptions): Promise<DownloadResult>;
23
- type SaveDownloadOptions = DownloadViaClickOptions & {
24
- /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
25
- savePath?: string;
26
- };
27
- /**
28
- * Convenience wrapper around {@link downloadViaClick} that also writes the
29
- * downloaded file to disk.
30
- */
31
- declare function downloadAndSave(page: Page, selector: string, options?: SaveDownloadOptions): Promise<DownloadResult & {
32
- savedTo: string;
33
- }>;
34
23
 
35
- export { type DownloadResult, type DownloadViaClickOptions, type SaveDownloadOptions, downloadAndSave, downloadViaClick };
24
+ export { type DownloadResult, type DownloadViaClickOptions, downloadViaClick };
@@ -1,5 +1,3 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
3
1
  async function downloadViaClick(page, selector, options) {
4
2
  const { logger, timeout = 3e4 } = options ?? {};
5
3
  const startTime = Date.now();
@@ -27,23 +25,6 @@ async function downloadViaClick(page, selector, options) {
27
25
  });
28
26
  return { buffer, filename };
29
27
  }
30
- async function downloadAndSave(page, selector, options) {
31
- const { savePath, ...downloadOpts } = options ?? {};
32
- const { buffer, filename } = await downloadViaClick(
33
- page,
34
- selector,
35
- downloadOpts
36
- );
37
- const dest = resolve(savePath ?? filename);
38
- await writeFile(dest, buffer);
39
- options?.logger?.info("download:saved", {
40
- filename,
41
- savedTo: dest,
42
- size: buffer.length
43
- });
44
- return { buffer, filename, savedTo: dest };
45
- }
46
28
  export {
47
- downloadAndSave,
48
29
  downloadViaClick
49
30
  };
@@ -1,3 +1,3 @@
1
- export { DownloadResult, DownloadViaClickOptions, SaveDownloadOptions, downloadAndSave, downloadViaClick } from './download.js';
1
+ export { DownloadResult, DownloadViaClickOptions, downloadViaClick } from './download.js';
2
2
  import 'playwright';
3
3
  import '../../shared/logger/logger.js';
@@ -1,8 +1,6 @@
1
1
  import {
2
- downloadViaClick,
3
- downloadAndSave
2
+ downloadViaClick
4
3
  } from "./download.js";
5
4
  export {
6
- downloadAndSave,
7
5
  downloadViaClick
8
6
  };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Load the nearest `.env` file above `scriptPath`.
3
+ * Existing `process.env` values are never overridden.
4
+ * Returns the path of the loaded `.env`, or `null` if none was found.
5
+ */
6
+ declare function loadProjectEnv(scriptPath: string): string | null;
7
+
8
+ export { loadProjectEnv };
@@ -0,0 +1,42 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ function findNearestEnv(startDir) {
4
+ let dir = startDir;
5
+ while (true) {
6
+ const envPath = join(dir, ".env");
7
+ if (existsSync(envPath)) return envPath;
8
+ const parent = dirname(dir);
9
+ if (parent === dir) return null;
10
+ dir = parent;
11
+ }
12
+ }
13
+ function parseEnvFile(content) {
14
+ const vars = {};
15
+ for (const raw of content.split("\n")) {
16
+ const line = raw.trim();
17
+ if (!line || line.startsWith("#")) continue;
18
+ const eqIndex = line.indexOf("=");
19
+ if (eqIndex === -1) continue;
20
+ const key = line.slice(0, eqIndex).trim();
21
+ let value = line.slice(eqIndex + 1).trim();
22
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
23
+ value = value.slice(1, -1);
24
+ }
25
+ vars[key] = value;
26
+ }
27
+ return vars;
28
+ }
29
+ function loadProjectEnv(scriptPath) {
30
+ const envPath = findNearestEnv(dirname(scriptPath));
31
+ if (!envPath) return null;
32
+ const vars = parseEnvFile(readFileSync(envPath, "utf8"));
33
+ for (const [key, value] of Object.entries(vars)) {
34
+ if (process.env[key] === void 0) {
35
+ process.env[key] = value;
36
+ }
37
+ }
38
+ return envPath;
39
+ }
40
+ export {
41
+ loadProjectEnv
42
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -4,6 +4,7 @@ import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { cwd } from "node:process";
5
5
  import { isAbsolute, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
+ import { loadProjectEnv } from "../../shared/env/load-env.js";
7
8
  import {
8
9
  getDefaultWorkflowFromModuleExports,
9
10
  getWorkflowsFromModuleExports,
@@ -194,6 +195,12 @@ async function runIntegrationInternal(
194
195
  ): Promise<RunIntegrationOutcome> {
195
196
  const { logger } = options;
196
197
  const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
198
+
199
+ const envPath = loadProjectEnv(absolutePath);
200
+ if (envPath) {
201
+ logger.info("loaded-env", { path: envPath });
202
+ }
203
+
197
204
  const workflow = await loadDefaultWorkflow(absolutePath);
198
205
  const signalPaths = getPauseSignalPaths(args.session);
199
206
  await removeSignalIfExists(signalPaths.pausedSignalPath);
package/src/index.ts CHANGED
@@ -53,10 +53,8 @@ export {
53
53
  // Download helpers
54
54
  export {
55
55
  downloadViaClick,
56
- downloadAndSave,
57
56
  type DownloadResult,
58
57
  type DownloadViaClickOptions,
59
- type SaveDownloadOptions,
60
58
  } from "./runtime/download/download.js";
61
59
 
62
60
  // Debug / Pause
@@ -1,5 +1,3 @@
1
- import { writeFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
3
1
  import type { Page, Download } from "playwright";
4
2
  import type { MinimalLogger } from "../../shared/logger/logger.js";
5
3
 
@@ -70,35 +68,3 @@ export async function downloadViaClick(
70
68
  return { buffer, filename };
71
69
  }
72
70
 
73
- export type SaveDownloadOptions = DownloadViaClickOptions & {
74
- /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
75
- savePath?: string;
76
- };
77
-
78
- /**
79
- * Convenience wrapper around {@link downloadViaClick} that also writes the
80
- * downloaded file to disk.
81
- */
82
- export async function downloadAndSave(
83
- page: Page,
84
- selector: string,
85
- options?: SaveDownloadOptions,
86
- ): Promise<DownloadResult & { savedTo: string }> {
87
- const { savePath, ...downloadOpts } = options ?? {};
88
- const { buffer, filename } = await downloadViaClick(
89
- page,
90
- selector,
91
- downloadOpts,
92
- );
93
-
94
- const dest = resolve(savePath ?? filename);
95
- await writeFile(dest, buffer);
96
-
97
- options?.logger?.info("download:saved", {
98
- filename,
99
- savedTo: dest,
100
- size: buffer.length,
101
- });
102
-
103
- return { buffer, filename, savedTo: dest };
104
- }
@@ -1,7 +1,5 @@
1
1
  export {
2
2
  downloadViaClick,
3
- downloadAndSave,
4
3
  type DownloadResult,
5
4
  type DownloadViaClickOptions,
6
- type SaveDownloadOptions,
7
5
  } from "./download.js";
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /**
5
+ * Walk up from `startDir` until a `.env` file is found.
6
+ * Returns the full path to the `.env`, or `null` if the filesystem root is reached.
7
+ */
8
+ function findNearestEnv(startDir: string): string | null {
9
+ let dir = startDir;
10
+ while (true) {
11
+ const envPath = join(dir, ".env");
12
+ if (existsSync(envPath)) return envPath;
13
+ const parent = dirname(dir);
14
+ if (parent === dir) return null; // filesystem root
15
+ dir = parent;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Parse a `.env` file into key-value pairs.
21
+ * Handles KEY=VALUE, KEY="VALUE", KEY='VALUE', comments (#), and blank lines.
22
+ * Does not support multiline values or variable interpolation.
23
+ */
24
+ function parseEnvFile(content: string): Record<string, string> {
25
+ const vars: Record<string, string> = {};
26
+ for (const raw of content.split("\n")) {
27
+ const line = raw.trim();
28
+ if (!line || line.startsWith("#")) continue;
29
+ const eqIndex = line.indexOf("=");
30
+ if (eqIndex === -1) continue;
31
+ const key = line.slice(0, eqIndex).trim();
32
+ let value = line.slice(eqIndex + 1).trim();
33
+ // Strip matching quotes
34
+ if (
35
+ (value.startsWith('"') && value.endsWith('"')) ||
36
+ (value.startsWith("'") && value.endsWith("'"))
37
+ ) {
38
+ value = value.slice(1, -1);
39
+ }
40
+ vars[key] = value;
41
+ }
42
+ return vars;
43
+ }
44
+
45
+ /**
46
+ * Load the nearest `.env` file above `scriptPath`.
47
+ * Existing `process.env` values are never overridden.
48
+ * Returns the path of the loaded `.env`, or `null` if none was found.
49
+ */
50
+ export function loadProjectEnv(scriptPath: string): string | null {
51
+ const envPath = findNearestEnv(dirname(scriptPath));
52
+ if (!envPath) return null;
53
+
54
+ const vars = parseEnvFile(readFileSync(envPath, "utf8"));
55
+ for (const [key, value] of Object.entries(vars)) {
56
+ if (process.env[key] === undefined) {
57
+ process.env[key] = value;
58
+ }
59
+ }
60
+ return envPath;
61
+ }