libretto 0.6.5 → 0.6.7-experimental.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/dist/cli/commands/setup.js +1 -1
- package/dist/cli/workers/run-integration-runtime.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -3
- package/dist/runtime/download/download.d.ts +1 -12
- package/dist/runtime/download/download.js +0 -19
- package/dist/runtime/download/index.d.ts +1 -1
- package/dist/runtime/download/index.js +1 -3
- package/dist/shared/env/load-env.d.ts +8 -0
- package/dist/shared/env/load-env.js +48 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/setup.ts +1 -1
- package/src/cli/workers/run-integration-runtime.ts +7 -0
- package/src/index.ts +0 -2
- package/src/runtime/download/download.ts +0 -34
- package/src/runtime/download/index.ts +0 -2
- package/src/shared/env/load-env.ts +70 -0
|
@@ -330,7 +330,7 @@ function copySkills() {
|
|
|
330
330
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
331
331
|
if (agentDirs.length === 0) {
|
|
332
332
|
console.log(
|
|
333
|
-
"\n\u26A0 No .agents/ or .claude/ directory found. Libretto skills were not installed."
|
|
333
|
+
"\n\u26A0\uFE0F No .agents/ or .claude/ directory found. Libretto skills were not installed."
|
|
334
334
|
);
|
|
335
335
|
console.log(
|
|
336
336
|
" Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:"
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
1
|
+
export { DownloadResult, DownloadViaClickOptions, downloadViaClick } from './download.js';
|
|
2
2
|
import 'playwright';
|
|
3
3
|
import '../../shared/logger/logger.js';
|
|
@@ -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,48 @@
|
|
|
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 withoutExport = line.startsWith("export ") ? line.slice("export ".length).trimStart() : line;
|
|
19
|
+
const eqIndex = withoutExport.indexOf("=");
|
|
20
|
+
if (eqIndex === -1) continue;
|
|
21
|
+
const key = withoutExport.slice(0, eqIndex).trim();
|
|
22
|
+
let value = withoutExport.slice(eqIndex + 1).trim();
|
|
23
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
} else {
|
|
26
|
+
const commentIndex = value.search(/\s#/);
|
|
27
|
+
if (commentIndex >= 0) {
|
|
28
|
+
value = value.slice(0, commentIndex).trimEnd();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
vars[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return vars;
|
|
34
|
+
}
|
|
35
|
+
function loadProjectEnv(scriptPath) {
|
|
36
|
+
const envPath = findNearestEnv(dirname(scriptPath));
|
|
37
|
+
if (!envPath) return null;
|
|
38
|
+
const vars = parseEnvFile(readFileSync(envPath, "utf8"));
|
|
39
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
40
|
+
if (process.env[key] === void 0) {
|
|
41
|
+
process.env[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return envPath;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
loadProjectEnv
|
|
48
|
+
};
|
package/package.json
CHANGED
package/skills/libretto/SKILL.md
CHANGED
|
@@ -431,7 +431,7 @@ function copySkills(): void {
|
|
|
431
431
|
|
|
432
432
|
if (agentDirs.length === 0) {
|
|
433
433
|
console.log(
|
|
434
|
-
"\n
|
|
434
|
+
"\n⚠️ No .agents/ or .claude/ directory found. Libretto skills were not installed.",
|
|
435
435
|
);
|
|
436
436
|
console.log(
|
|
437
437
|
" Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:",
|
|
@@ -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
|
@@ -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
|
-
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 withoutExport = line.startsWith("export ")
|
|
30
|
+
? line.slice("export ".length).trimStart()
|
|
31
|
+
: line;
|
|
32
|
+
const eqIndex = withoutExport.indexOf("=");
|
|
33
|
+
if (eqIndex === -1) continue;
|
|
34
|
+
const key = withoutExport.slice(0, eqIndex).trim();
|
|
35
|
+
let value = withoutExport.slice(eqIndex + 1).trim();
|
|
36
|
+
// Strip matching quotes
|
|
37
|
+
if (
|
|
38
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
40
|
+
) {
|
|
41
|
+
value = value.slice(1, -1);
|
|
42
|
+
} else {
|
|
43
|
+
// Strip inline comments from unquoted values
|
|
44
|
+
const commentIndex = value.search(/\s#/);
|
|
45
|
+
if (commentIndex >= 0) {
|
|
46
|
+
value = value.slice(0, commentIndex).trimEnd();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
vars[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return vars;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Load the nearest `.env` file above `scriptPath`.
|
|
56
|
+
* Existing `process.env` values are never overridden.
|
|
57
|
+
* Returns the path of the loaded `.env`, or `null` if none was found.
|
|
58
|
+
*/
|
|
59
|
+
export function loadProjectEnv(scriptPath: string): string | null {
|
|
60
|
+
const envPath = findNearestEnv(dirname(scriptPath));
|
|
61
|
+
if (!envPath) return null;
|
|
62
|
+
|
|
63
|
+
const vars = parseEnvFile(readFileSync(envPath, "utf8"));
|
|
64
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
65
|
+
if (process.env[key] === undefined) {
|
|
66
|
+
process.env[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return envPath;
|
|
70
|
+
}
|