libretto 0.5.3-experimental.5 → 0.5.3

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.
Files changed (126) hide show
  1. package/README.md +114 -37
  2. package/README.template.md +160 -0
  3. package/dist/cli/cli.js +22 -97
  4. package/dist/cli/commands/browser.js +86 -59
  5. package/dist/cli/commands/deploy.js +148 -0
  6. package/dist/cli/commands/execution.js +218 -96
  7. package/dist/cli/commands/init.js +34 -29
  8. package/dist/cli/commands/logs.js +4 -5
  9. package/dist/cli/commands/shared.js +30 -29
  10. package/dist/cli/commands/snapshot.js +26 -39
  11. package/dist/cli/core/ai-config.js +21 -4
  12. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  13. package/dist/cli/core/browser.js +207 -37
  14. package/dist/cli/core/context.js +4 -1
  15. package/dist/cli/core/deploy-artifact.js +687 -0
  16. package/dist/cli/core/session-telemetry.js +434 -174
  17. package/dist/cli/core/session.js +21 -8
  18. package/dist/cli/core/snapshot-analyzer.js +14 -31
  19. package/dist/cli/core/snapshot-api-config.js +2 -6
  20. package/dist/cli/core/telemetry.js +20 -4
  21. package/dist/cli/framework/simple-cli.js +144 -43
  22. package/dist/cli/router.js +16 -21
  23. package/dist/cli/workers/run-integration-runtime.js +25 -45
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/cli/workers/run-integration-worker.js +1 -4
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +13 -10
  28. package/dist/runtime/download/download.js +5 -1
  29. package/dist/runtime/extract/extract.js +11 -2
  30. package/dist/runtime/network/network.js +8 -1
  31. package/dist/runtime/recovery/agent.js +6 -2
  32. package/dist/runtime/recovery/errors.js +3 -1
  33. package/dist/runtime/recovery/recovery.js +3 -1
  34. package/dist/shared/condense-dom/condense-dom.js +17 -69
  35. package/dist/shared/config/config.d.ts +1 -9
  36. package/dist/shared/config/config.js +0 -18
  37. package/dist/shared/config/index.d.ts +2 -1
  38. package/dist/shared/config/index.js +0 -10
  39. package/dist/shared/debug/pause.js +9 -3
  40. package/dist/shared/dom-semantics.d.ts +8 -0
  41. package/dist/shared/dom-semantics.js +69 -0
  42. package/dist/shared/instrumentation/instrument.js +101 -5
  43. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  44. package/dist/shared/llm/client.js +3 -1
  45. package/dist/shared/logger/index.js +4 -1
  46. package/dist/shared/run/api.js +3 -1
  47. package/dist/shared/run/browser.js +47 -3
  48. package/dist/shared/state/session-state.d.ts +2 -1
  49. package/dist/shared/state/session-state.js +5 -2
  50. package/dist/shared/visualization/ghost-cursor.js +36 -14
  51. package/dist/shared/visualization/highlight.js +9 -6
  52. package/dist/shared/workflow/workflow.d.ts +18 -10
  53. package/dist/shared/workflow/workflow.js +50 -5
  54. package/package.json +14 -6
  55. package/scripts/generate-changelog.ts +132 -0
  56. package/scripts/postinstall.mjs +4 -3
  57. package/scripts/skills-libretto.mjs +2 -88
  58. package/scripts/summarize-evals.mjs +32 -10
  59. package/skills/libretto/SKILL.md +132 -62
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +176 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/deploy.ts +198 -0
  69. package/src/cli/commands/execution.ts +251 -111
  70. package/src/cli/commands/init.ts +37 -33
  71. package/src/cli/commands/logs.ts +7 -7
  72. package/src/cli/commands/shared.ts +36 -37
  73. package/src/cli/commands/snapshot.ts +44 -59
  74. package/src/cli/core/ai-config.ts +24 -4
  75. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  76. package/src/cli/core/browser.ts +260 -49
  77. package/src/cli/core/context.ts +7 -2
  78. package/src/cli/core/deploy-artifact.ts +938 -0
  79. package/src/cli/core/session-telemetry.ts +449 -197
  80. package/src/cli/core/session.ts +21 -7
  81. package/src/cli/core/snapshot-analyzer.ts +26 -46
  82. package/src/cli/core/snapshot-api-config.ts +170 -175
  83. package/src/cli/core/telemetry.ts +39 -4
  84. package/src/cli/framework/simple-cli.ts +281 -98
  85. package/src/cli/router.ts +15 -21
  86. package/src/cli/workers/run-integration-runtime.ts +35 -57
  87. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  88. package/src/cli/workers/run-integration-worker.ts +1 -4
  89. package/src/index.ts +77 -67
  90. package/src/runtime/download/download.ts +62 -58
  91. package/src/runtime/download/index.ts +5 -5
  92. package/src/runtime/extract/extract.ts +71 -61
  93. package/src/runtime/network/index.ts +3 -3
  94. package/src/runtime/network/network.ts +99 -93
  95. package/src/runtime/recovery/agent.ts +217 -212
  96. package/src/runtime/recovery/errors.ts +107 -104
  97. package/src/runtime/recovery/index.ts +3 -3
  98. package/src/runtime/recovery/recovery.ts +38 -35
  99. package/src/shared/condense-dom/condense-dom.ts +27 -82
  100. package/src/shared/config/config.ts +0 -19
  101. package/src/shared/config/index.ts +0 -5
  102. package/src/shared/debug/pause.ts +57 -51
  103. package/src/shared/dom-semantics.ts +68 -0
  104. package/src/shared/instrumentation/errors.ts +64 -62
  105. package/src/shared/instrumentation/index.ts +5 -5
  106. package/src/shared/instrumentation/instrument.ts +339 -209
  107. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  108. package/src/shared/llm/client.ts +181 -174
  109. package/src/shared/llm/types.ts +39 -39
  110. package/src/shared/logger/index.ts +11 -4
  111. package/src/shared/logger/logger.ts +312 -306
  112. package/src/shared/logger/sinks.ts +118 -114
  113. package/src/shared/paths/paths.ts +50 -49
  114. package/src/shared/paths/repo-root.ts +17 -17
  115. package/src/shared/run/api.ts +5 -1
  116. package/src/shared/run/browser.ts +65 -3
  117. package/src/shared/state/index.ts +9 -9
  118. package/src/shared/state/session-state.ts +46 -43
  119. package/src/shared/visualization/ghost-cursor.ts +180 -149
  120. package/src/shared/visualization/highlight.ts +89 -86
  121. package/src/shared/visualization/index.ts +13 -13
  122. package/src/shared/workflow/workflow.ts +107 -30
  123. package/scripts/check-skills-sync.mjs +0 -23
  124. package/scripts/prepare-release.sh +0 -97
  125. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
  126. package/skills/libretto/references/user-action-log.md +0 -31
@@ -0,0 +1,938 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { dirname, isAbsolute, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { gzipSync } from "node:zlib";
17
+ import { build } from "esbuild";
18
+
19
+ type PackageManifest = {
20
+ name?: string;
21
+ version?: string;
22
+ packageManager?: string;
23
+ main?: string;
24
+ module?: string;
25
+ source?: string;
26
+ types?: string;
27
+ exports?: unknown;
28
+ dependencies?: Record<string, string>;
29
+ devDependencies?: Record<string, string>;
30
+ peerDependencies?: Record<string, string>;
31
+ optionalDependencies?: Record<string, string>;
32
+ workspaces?: string[] | { packages?: string[] };
33
+ };
34
+
35
+ type WorkspacePackage = {
36
+ dir: string;
37
+ manifest: PackageManifest;
38
+ name: string;
39
+ };
40
+
41
+ type HostedDeployPackage = {
42
+ cleanup: () => void;
43
+ entryPoint: string;
44
+ outputDir: string;
45
+ };
46
+
47
+ type BuildHostedDeployTarballArgs = {
48
+ additionalExternals?: readonly string[];
49
+ deploymentName: string;
50
+ entryPoint?: string;
51
+ sourceDir: string;
52
+ };
53
+
54
+ type CreateHostedDeployPackageArgs = BuildHostedDeployTarballArgs;
55
+
56
+ const DEFAULT_RUNTIME_EXTERNALS = [
57
+ "libretto",
58
+ "playwright",
59
+ "playwright-core",
60
+ "chromium-bidi",
61
+ ] as const;
62
+ const BUILT_IN_MANIFEST_DEPENDENCIES = ["libretto"] as const;
63
+ const SOURCE_FILE_EXTENSIONS = [
64
+ "",
65
+ ".ts",
66
+ ".tsx",
67
+ ".mts",
68
+ ".cts",
69
+ ".js",
70
+ ".mjs",
71
+ ".cjs",
72
+ "/index.ts",
73
+ "/index.tsx",
74
+ "/index.mts",
75
+ "/index.cts",
76
+ "/index.js",
77
+ "/index.mjs",
78
+ "/index.cjs",
79
+ ] as const;
80
+ const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
81
+ const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
82
+ new URL("../../..", import.meta.url),
83
+ );
84
+
85
+ function readCurrentLibrettoVersion(): string {
86
+ const packageJsonPath = fileURLToPath(
87
+ new URL("../../../package.json", import.meta.url),
88
+ );
89
+ const manifest = readJsonFile<PackageManifest>(packageJsonPath);
90
+ if (!manifest.version) {
91
+ throw new Error(
92
+ `Unable to determine current libretto version from ${packageJsonPath}.`,
93
+ );
94
+ }
95
+ return manifest.version;
96
+ }
97
+
98
+ function readJsonFile<T>(path: string): T {
99
+ return JSON.parse(readFileSync(path, "utf8")) as T;
100
+ }
101
+
102
+ function readPackageManifest(path: string): PackageManifest {
103
+ return readJsonFile<PackageManifest>(path);
104
+ }
105
+
106
+ function ensureSourcePackageManifest(sourceDir: string): PackageManifest {
107
+ const pkgJsonPath = join(sourceDir, "package.json");
108
+ if (!existsSync(pkgJsonPath)) {
109
+ throw new Error(
110
+ `No package.json found in ${sourceDir}. Deploy source must contain a package.json.`,
111
+ );
112
+ }
113
+ return readPackageManifest(pkgJsonPath);
114
+ }
115
+
116
+ function resolveEntryPointPath(sourceDir: string, entryPoint?: string): string {
117
+ const candidate = entryPoint ?? "index.ts";
118
+ const absEntryPoint = isAbsolute(candidate)
119
+ ? resolve(candidate)
120
+ : resolve(sourceDir, candidate);
121
+
122
+ if (!existsSync(absEntryPoint)) {
123
+ throw new Error(
124
+ `Deploy entry point not found: ${absEntryPoint}. Pass --entry-point to choose a workflow file.`,
125
+ );
126
+ }
127
+
128
+ return absEntryPoint;
129
+ }
130
+
131
+ function isRootPath(path: string): boolean {
132
+ return dirname(path) === path;
133
+ }
134
+
135
+ function findWorkspaceRoot(startDir: string): string | null {
136
+ let currentDir = resolve(startDir);
137
+
138
+ while (true) {
139
+ if (existsSync(join(currentDir, "pnpm-workspace.yaml"))) {
140
+ return currentDir;
141
+ }
142
+
143
+ const pkgJsonPath = join(currentDir, "package.json");
144
+ if (existsSync(pkgJsonPath)) {
145
+ const manifest = readPackageManifest(pkgJsonPath);
146
+ if (manifest.workspaces) {
147
+ return currentDir;
148
+ }
149
+ }
150
+
151
+ if (isRootPath(currentDir)) {
152
+ return null;
153
+ }
154
+ currentDir = dirname(currentDir);
155
+ }
156
+ }
157
+
158
+ function readWorkspacePatterns(rootDir: string): string[] {
159
+ const pnpmWorkspacePath = join(rootDir, "pnpm-workspace.yaml");
160
+ if (existsSync(pnpmWorkspacePath)) {
161
+ const patterns: string[] = [];
162
+ let inPackagesBlock = false;
163
+
164
+ for (const rawLine of readFileSync(pnpmWorkspacePath, "utf8").split(
165
+ /\r?\n/,
166
+ )) {
167
+ const trimmed = rawLine.trim();
168
+ if (!inPackagesBlock) {
169
+ if (trimmed === "packages:") {
170
+ inPackagesBlock = true;
171
+ }
172
+ continue;
173
+ }
174
+
175
+ if (
176
+ trimmed.length > 0 &&
177
+ !trimmed.startsWith("-") &&
178
+ !rawLine.startsWith(" ") &&
179
+ !rawLine.startsWith("\t")
180
+ ) {
181
+ break;
182
+ }
183
+
184
+ const match = trimmed.match(/^-\s*["']?(.+?)["']?$/);
185
+ if (match?.[1]) {
186
+ patterns.push(match[1]);
187
+ }
188
+ }
189
+
190
+ if (patterns.length > 0) {
191
+ return patterns;
192
+ }
193
+ }
194
+
195
+ const pkgJsonPath = join(rootDir, "package.json");
196
+ if (!existsSync(pkgJsonPath)) {
197
+ return [];
198
+ }
199
+
200
+ const manifest = readPackageManifest(pkgJsonPath);
201
+ if (Array.isArray(manifest.workspaces)) {
202
+ return manifest.workspaces;
203
+ }
204
+ if (manifest.workspaces && Array.isArray(manifest.workspaces.packages)) {
205
+ return manifest.workspaces.packages;
206
+ }
207
+
208
+ return [];
209
+ }
210
+
211
+ function expandWorkspacePattern(rootDir: string, pattern: string): string[] {
212
+ if (!pattern.includes("*")) {
213
+ const absDir = resolve(rootDir, pattern);
214
+ return existsSync(absDir) ? [absDir] : [];
215
+ }
216
+
217
+ if (!pattern.endsWith("/*")) {
218
+ return [];
219
+ }
220
+
221
+ const baseDir = resolve(rootDir, pattern.slice(0, -2));
222
+ if (!existsSync(baseDir)) {
223
+ return [];
224
+ }
225
+
226
+ return readdirSync(baseDir, { withFileTypes: true })
227
+ .filter((entry) => entry.isDirectory())
228
+ .map((entry) => join(baseDir, entry.name));
229
+ }
230
+
231
+ function discoverWorkspacePackages(
232
+ startDir: string,
233
+ ): Map<string, WorkspacePackage> {
234
+ const workspaceRoot = findWorkspaceRoot(startDir);
235
+ if (!workspaceRoot) {
236
+ return new Map();
237
+ }
238
+
239
+ const packages = new Map<string, WorkspacePackage>();
240
+ for (const pattern of readWorkspacePatterns(workspaceRoot)) {
241
+ for (const dir of expandWorkspacePattern(workspaceRoot, pattern)) {
242
+ const pkgJsonPath = join(dir, "package.json");
243
+ if (!existsSync(pkgJsonPath)) {
244
+ continue;
245
+ }
246
+ const manifest = readPackageManifest(pkgJsonPath);
247
+ if (!manifest.name) {
248
+ continue;
249
+ }
250
+ packages.set(manifest.name, { dir, manifest, name: manifest.name });
251
+ }
252
+ }
253
+
254
+ return packages;
255
+ }
256
+
257
+ function findMatchingWorkspacePackage(
258
+ importPath: string,
259
+ workspacePackages: Map<string, WorkspacePackage>,
260
+ ): {
261
+ info: WorkspacePackage;
262
+ subpath: string;
263
+ } | null {
264
+ const names = [...workspacePackages.keys()].sort(
265
+ (left, right) => right.length - left.length,
266
+ );
267
+
268
+ for (const name of names) {
269
+ if (importPath === name) {
270
+ return {
271
+ info: workspacePackages.get(name)!,
272
+ subpath: ".",
273
+ };
274
+ }
275
+ if (importPath.startsWith(`${name}/`)) {
276
+ return {
277
+ info: workspacePackages.get(name)!,
278
+ subpath: `.${importPath.slice(name.length)}`,
279
+ };
280
+ }
281
+ }
282
+
283
+ return null;
284
+ }
285
+
286
+ function resolvePathCandidates(
287
+ packageDir: string,
288
+ target: string,
289
+ replacement?: string,
290
+ ): string | null {
291
+ const value = replacement ? target.replace(/\*/g, replacement) : target;
292
+ const absCandidate = resolve(packageDir, value);
293
+ if (existsSync(absCandidate)) {
294
+ return absCandidate;
295
+ }
296
+
297
+ for (const suffix of SOURCE_FILE_EXTENSIONS) {
298
+ const fileCandidate = resolve(packageDir, `${value}${suffix}`);
299
+ if (existsSync(fileCandidate)) {
300
+ return fileCandidate;
301
+ }
302
+ }
303
+
304
+ return null;
305
+ }
306
+
307
+ function resolveExportTarget(
308
+ exportValue: unknown,
309
+ packageDir: string,
310
+ replacement?: string,
311
+ ): string | null {
312
+ if (typeof exportValue === "string") {
313
+ return resolvePathCandidates(packageDir, exportValue, replacement);
314
+ }
315
+
316
+ if (Array.isArray(exportValue)) {
317
+ for (const entry of exportValue) {
318
+ const resolved = resolveExportTarget(entry, packageDir, replacement);
319
+ if (resolved) {
320
+ return resolved;
321
+ }
322
+ }
323
+ return null;
324
+ }
325
+
326
+ if (!exportValue || typeof exportValue !== "object") {
327
+ return null;
328
+ }
329
+
330
+ const record = exportValue as Record<string, unknown>;
331
+ for (const condition of [
332
+ "types",
333
+ "source",
334
+ "import",
335
+ "default",
336
+ "module",
337
+ "require",
338
+ ]) {
339
+ if (!(condition in record)) {
340
+ continue;
341
+ }
342
+ const resolved = resolveExportTarget(
343
+ record[condition],
344
+ packageDir,
345
+ replacement,
346
+ );
347
+ if (resolved) {
348
+ return resolved;
349
+ }
350
+ }
351
+
352
+ for (const value of Object.values(record)) {
353
+ const resolved = resolveExportTarget(value, packageDir, replacement);
354
+ if (resolved) {
355
+ return resolved;
356
+ }
357
+ }
358
+
359
+ return null;
360
+ }
361
+
362
+ function resolveExportsSubpath(
363
+ exportsField: unknown,
364
+ packageDir: string,
365
+ subpath: string,
366
+ ): string | null {
367
+ if (!exportsField) {
368
+ return null;
369
+ }
370
+
371
+ if (
372
+ subpath === "." &&
373
+ (typeof exportsField === "string" || Array.isArray(exportsField))
374
+ ) {
375
+ const rootExport = resolveExportTarget(exportsField, packageDir);
376
+ if (rootExport) {
377
+ return rootExport;
378
+ }
379
+ }
380
+
381
+ if (typeof exportsField !== "object" || Array.isArray(exportsField)) {
382
+ return null;
383
+ }
384
+
385
+ const record = exportsField as Record<string, unknown>;
386
+ const hasExplicitSubpathKeys = Object.keys(record).some((key) =>
387
+ key.startsWith("."),
388
+ );
389
+
390
+ if (!hasExplicitSubpathKeys) {
391
+ return subpath === "." ? resolveExportTarget(record, packageDir) : null;
392
+ }
393
+
394
+ const exactMatch = record[subpath];
395
+ if (exactMatch !== undefined) {
396
+ return resolveExportTarget(exactMatch, packageDir);
397
+ }
398
+
399
+ for (const [key, value] of Object.entries(record)) {
400
+ const starIndex = key.indexOf("*");
401
+ if (starIndex < 0) {
402
+ continue;
403
+ }
404
+ const prefix = key.slice(0, starIndex);
405
+ const suffix = key.slice(starIndex + 1);
406
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) {
407
+ continue;
408
+ }
409
+ const replacement = subpath.slice(
410
+ prefix.length,
411
+ subpath.length - suffix.length,
412
+ );
413
+ const resolved = resolveExportTarget(value, packageDir, replacement);
414
+ if (resolved) {
415
+ return resolved;
416
+ }
417
+ }
418
+
419
+ return null;
420
+ }
421
+
422
+ function resolveWorkspaceSourcePath(
423
+ info: WorkspacePackage,
424
+ subpath: string,
425
+ ): string | null {
426
+ const viaExports = resolveExportsSubpath(
427
+ info.manifest.exports,
428
+ info.dir,
429
+ subpath,
430
+ );
431
+ if (viaExports) {
432
+ return viaExports;
433
+ }
434
+
435
+ if (subpath === ".") {
436
+ for (const field of [
437
+ info.manifest.types,
438
+ info.manifest.source,
439
+ info.manifest.module,
440
+ info.manifest.main,
441
+ ]) {
442
+ if (!field) {
443
+ continue;
444
+ }
445
+ const resolved = resolvePathCandidates(info.dir, field);
446
+ if (resolved) {
447
+ return resolved;
448
+ }
449
+ }
450
+ }
451
+
452
+ const directSubpath = subpath === "." ? "index" : subpath.slice(2);
453
+ return resolvePathCandidates(info.dir, directSubpath);
454
+ }
455
+
456
+ function workspaceSourcePlugin(
457
+ workspacePackages: Map<string, WorkspacePackage>,
458
+ externalPackages: ReadonlySet<string>,
459
+ ) {
460
+ return {
461
+ name: "workspace-source-resolver",
462
+ setup(buildApi: {
463
+ onResolve: (
464
+ options: { filter: RegExp },
465
+ callback: (args: { path: string }) => { path: string } | null,
466
+ ) => void;
467
+ }) {
468
+ // Workspace imports are treated as bundle input, so their code is
469
+ // embedded into the generated implementation file. The deployed package
470
+ // does not depend on the original monorepo layout or workspace:* links.
471
+ buildApi.onResolve({ filter: /^[^./].*/ }, (args) => {
472
+ if (externalPackages.has(args.path)) {
473
+ return null;
474
+ }
475
+
476
+ const match = findMatchingWorkspacePackage(
477
+ args.path,
478
+ workspacePackages,
479
+ );
480
+ if (!match) {
481
+ return null;
482
+ }
483
+
484
+ const resolvedPath = resolveWorkspaceSourcePath(
485
+ match.info,
486
+ match.subpath,
487
+ );
488
+ if (!resolvedPath) {
489
+ throw new Error(
490
+ `Unable to resolve workspace import "${args.path}" from ${match.info.dir}.`,
491
+ );
492
+ }
493
+
494
+ return { path: resolvedPath };
495
+ });
496
+ },
497
+ };
498
+ }
499
+
500
+ function normalizePackageName(name: string): string {
501
+ const normalized = name
502
+ .toLowerCase()
503
+ .replace(/[^a-z0-9._-]+/g, "-")
504
+ .replace(/^-+|-+$/g, "");
505
+ return normalized || "libretto-deployment";
506
+ }
507
+
508
+ function readDependencyVersionFromManifest(
509
+ manifest: PackageManifest,
510
+ packageName: string,
511
+ ): string | null {
512
+ for (const dependencyGroup of [
513
+ manifest.dependencies,
514
+ manifest.devDependencies,
515
+ manifest.peerDependencies,
516
+ manifest.optionalDependencies,
517
+ ]) {
518
+ const version = dependencyGroup?.[packageName];
519
+ if (version) {
520
+ return version;
521
+ }
522
+ }
523
+
524
+ return null;
525
+ }
526
+
527
+ function resolveDependencyVersion(
528
+ sourceDir: string,
529
+ packageName: string,
530
+ fallbackVersion?: string,
531
+ ): string {
532
+ let currentDir = resolve(sourceDir);
533
+
534
+ while (true) {
535
+ const pkgJsonPath = join(currentDir, "package.json");
536
+ if (existsSync(pkgJsonPath)) {
537
+ const version = readDependencyVersionFromManifest(
538
+ readPackageManifest(pkgJsonPath),
539
+ packageName,
540
+ );
541
+ if (version) {
542
+ return version;
543
+ }
544
+ }
545
+
546
+ if (isRootPath(currentDir)) {
547
+ break;
548
+ }
549
+ currentDir = dirname(currentDir);
550
+ }
551
+
552
+ if (fallbackVersion) {
553
+ return fallbackVersion;
554
+ }
555
+
556
+ throw new Error(
557
+ `Unable to determine a version for external package "${packageName}". Add it to your package.json or remove it from --external.`,
558
+ );
559
+ }
560
+
561
+ function writeDeployManifest(args: {
562
+ additionalExternals: readonly string[];
563
+ deploymentName: string;
564
+ librettoDependency: string;
565
+ outputDir: string;
566
+ sourceDir: string;
567
+ }): void {
568
+ const dependencies = Object.fromEntries(
569
+ [...BUILT_IN_MANIFEST_DEPENDENCIES, ...args.additionalExternals].map(
570
+ (packageName) => [
571
+ packageName,
572
+ packageName === "libretto"
573
+ ? args.librettoDependency
574
+ : resolveDependencyVersion(args.sourceDir, packageName),
575
+ ],
576
+ ),
577
+ );
578
+
579
+ writeFileSync(
580
+ join(args.outputDir, "package.json"),
581
+ JSON.stringify(
582
+ {
583
+ name: normalizePackageName(args.deploymentName),
584
+ private: true,
585
+ type: "module",
586
+ dependencies,
587
+ },
588
+ null,
589
+ 2,
590
+ ) + "\n",
591
+ );
592
+ }
593
+
594
+ function shouldVendorCurrentLibretto(versionSpec: string): boolean {
595
+ return (
596
+ versionSpec.startsWith("file:") ||
597
+ versionSpec.startsWith("link:") ||
598
+ versionSpec.startsWith("workspace:") ||
599
+ versionSpec.startsWith("portal:") ||
600
+ versionSpec.includes("&path:")
601
+ );
602
+ }
603
+
604
+ function resolveLibrettoDependency(sourceDir: string): string {
605
+ const versionSpec = resolveDependencyVersion(
606
+ sourceDir,
607
+ "libretto",
608
+ CURRENT_LIBRETTO_VERSION,
609
+ );
610
+
611
+ if (shouldVendorCurrentLibretto(versionSpec)) {
612
+ return "file:./libretto";
613
+ }
614
+
615
+ return versionSpec;
616
+ }
617
+
618
+ function copyCurrentLibrettoPackage(outputDir: string): void {
619
+ const bundledLibrettoDir = join(outputDir, "libretto");
620
+ mkdirSync(bundledLibrettoDir, { recursive: true });
621
+ cpSync(
622
+ join(CURRENT_LIBRETTO_PACKAGE_DIR, "dist"),
623
+ join(bundledLibrettoDir, "dist"),
624
+ { recursive: true },
625
+ );
626
+ cpSync(
627
+ join(CURRENT_LIBRETTO_PACKAGE_DIR, "package.json"),
628
+ join(bundledLibrettoDir, "package.json"),
629
+ );
630
+ }
631
+
632
+ function formatBuildError(error: unknown): string {
633
+ if (!(error instanceof Error)) {
634
+ return String(error);
635
+ }
636
+
637
+ const candidate = error as Error & {
638
+ errors?: Array<{
639
+ location?: { file?: string; line?: number; column?: number };
640
+ text?: string;
641
+ }>;
642
+ };
643
+ if (!Array.isArray(candidate.errors) || candidate.errors.length === 0) {
644
+ return error.message;
645
+ }
646
+
647
+ return candidate.errors
648
+ .map((entry) => {
649
+ const location = entry.location?.file
650
+ ? `${entry.location.file}:${entry.location.line ?? 0}:${entry.location.column ?? 0}`
651
+ : "unknown";
652
+ return `${location} ${entry.text ?? error.message}`;
653
+ })
654
+ .join("\n");
655
+ }
656
+
657
+ function extractExportNamesFromEsmBundle(bundleSource: string): string[] {
658
+ const exportNames = new Set<string>();
659
+
660
+ for (const entry of bundleSource.matchAll(
661
+ /export\s+(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g,
662
+ )) {
663
+ exportNames.add(entry[1]!);
664
+ }
665
+
666
+ for (const entry of bundleSource.matchAll(/export\s+\{([^}]+)\};/g)) {
667
+ const specifiers = entry[1]?.split(",") ?? [];
668
+ for (const specifier of specifiers) {
669
+ const trimmed = specifier.trim();
670
+ if (!trimmed) {
671
+ continue;
672
+ }
673
+ const aliasMatch = trimmed.match(
674
+ /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*|default)$/,
675
+ );
676
+ if (aliasMatch?.[2]) {
677
+ exportNames.add(aliasMatch[2]);
678
+ continue;
679
+ }
680
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
681
+ exportNames.add(trimmed);
682
+ }
683
+ }
684
+ }
685
+
686
+ if (/\bexport\s+default\b/m.test(bundleSource)) {
687
+ exportNames.add("default");
688
+ }
689
+
690
+ return [...exportNames];
691
+ }
692
+
693
+ function createBootstrapSource(args: {
694
+ bundleBuffer: Buffer;
695
+ deploymentName: string;
696
+ exportNames: readonly string[];
697
+ }): string {
698
+ const bundleHash = createHash("sha256")
699
+ .update(args.bundleBuffer)
700
+ .digest("hex")
701
+ .slice(0, 16);
702
+ const bundleBase64 = gzipSync(args.bundleBuffer, { level: 9 }).toString(
703
+ "base64",
704
+ );
705
+ const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
706
+ const hasDefaultExport = args.exportNames.includes("default");
707
+ const exportLines = args.exportNames
708
+ .filter((name) => name !== "default")
709
+ .map(
710
+ (name) =>
711
+ `export const ${name} = createWorkflowProxy(${JSON.stringify(name)});`,
712
+ )
713
+ .join("\n");
714
+ const defaultExportLine = hasDefaultExport
715
+ ? 'export default createWorkflowProxy("default");'
716
+ : "";
717
+
718
+ // The deploy entrypoint is tiny on purpose. Hosted build imports this module
719
+ // to discover workflow exports. The implementation bundle stays embedded in
720
+ // the file, while external packages are resolved from node_modules when the
721
+ // deployed code loads them.
722
+ return `import { createRequire } from "node:module";
723
+ import { existsSync, writeFileSync } from "node:fs";
724
+ import { tmpdir } from "node:os";
725
+ import { join } from "node:path";
726
+ import { gunzipSync } from "node:zlib";
727
+ import { workflow } from "libretto";
728
+
729
+ const BUNDLE_HASH = ${JSON.stringify(bundleHash)};
730
+ const BUNDLE_GZIP_BASE64 = ${JSON.stringify(bundleBase64)};
731
+ const BUNDLE_FILENAME = join(
732
+ tmpdir(),
733
+ ${JSON.stringify(outputPrefix)} + BUNDLE_HASH + ".cjs",
734
+ );
735
+ const nativeRequire = createRequire(
736
+ join(tmpdir(), ${JSON.stringify("libretto-deploy-bootstrap.cjs")}),
737
+ );
738
+
739
+ function ensureBundleFile() {
740
+ if (!existsSync(BUNDLE_FILENAME)) {
741
+ writeFileSync(
742
+ BUNDLE_FILENAME,
743
+ gunzipSync(Buffer.from(BUNDLE_GZIP_BASE64, "base64")),
744
+ );
745
+ }
746
+
747
+ return BUNDLE_FILENAME;
748
+ }
749
+
750
+ function createWorkflowProxy(exportName) {
751
+ return workflow(exportName, async (ctx, input) => {
752
+ const impl = nativeRequire(ensureBundleFile());
753
+ const target = impl[exportName];
754
+ if (!target || typeof target.run !== "function") {
755
+ throw new Error(
756
+ \`Expected workflow export "\${exportName}" to be available in the bundled deployment implementation.\`,
757
+ );
758
+ }
759
+ return await target.run(ctx, input);
760
+ });
761
+ }
762
+
763
+ ${exportLines}
764
+ ${defaultExportLine}
765
+ `;
766
+ }
767
+
768
+ async function writeBundledDeployEntrypoint(args: {
769
+ absEntryPoint: string;
770
+ absSourceDir: string;
771
+ deploymentName: string;
772
+ externalPackages: ReadonlySet<string>;
773
+ outputDir: string;
774
+ workspacePackages: Map<string, WorkspacePackage>;
775
+ }): Promise<void> {
776
+ try {
777
+ // The implementation bundle is CommonJS so the bootstrap can load it lazily
778
+ // with createRequire() after workflow discovery, while external packages
779
+ // continue to load through normal Node module resolution.
780
+ const implementationBuild = await build({
781
+ absWorkingDir: args.absSourceDir,
782
+ bundle: true,
783
+ entryPoints: [args.absEntryPoint],
784
+ external: [...args.externalPackages],
785
+ format: "cjs",
786
+ outfile: "prebundled.cjs",
787
+ platform: "node",
788
+ plugins: [
789
+ workspaceSourcePlugin(args.workspacePackages, args.externalPackages),
790
+ ],
791
+ splitting: false,
792
+ target: "node20",
793
+ write: false,
794
+ });
795
+
796
+ const bundledImplementation = implementationBuild.outputFiles?.find(
797
+ (file) => file.path.endsWith("prebundled.cjs"),
798
+ );
799
+ if (!bundledImplementation) {
800
+ throw new Error(
801
+ "Bundler did not produce a deployment implementation file.",
802
+ );
803
+ }
804
+
805
+ // A separate ESM bundle is used only to read the entry module's exported
806
+ // workflow names. Scanning the CommonJS bundle would also see exports from
807
+ // bundled dependencies, which is not the deploy surface.
808
+ const exportBuild = await build({
809
+ absWorkingDir: args.absSourceDir,
810
+ bundle: true,
811
+ entryPoints: [args.absEntryPoint],
812
+ external: [...args.externalPackages],
813
+ format: "esm",
814
+ outfile: "entry-exports.js",
815
+ platform: "node",
816
+ plugins: [
817
+ workspaceSourcePlugin(args.workspacePackages, args.externalPackages),
818
+ ],
819
+ splitting: false,
820
+ target: "node20",
821
+ write: false,
822
+ });
823
+
824
+ const bundledExports = exportBuild.outputFiles?.find((file) =>
825
+ file.path.endsWith("entry-exports.js"),
826
+ );
827
+ if (!bundledExports) {
828
+ throw new Error("Bundler did not produce an export analysis file.");
829
+ }
830
+
831
+ const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
832
+ if (exportNames.length === 0) {
833
+ throw new Error(
834
+ `No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`,
835
+ );
836
+ }
837
+
838
+ writeFileSync(
839
+ join(args.outputDir, "index.js"),
840
+ createBootstrapSource({
841
+ bundleBuffer: Buffer.from(bundledImplementation.contents),
842
+ deploymentName: args.deploymentName,
843
+ exportNames,
844
+ }),
845
+ );
846
+ } catch (error) {
847
+ throw new Error(
848
+ `Failed to bundle deploy entry point ${args.absEntryPoint}.\n${formatBuildError(error)}`,
849
+ );
850
+ }
851
+ }
852
+
853
+ export async function createHostedDeployPackage(
854
+ args: CreateHostedDeployPackageArgs,
855
+ ): Promise<HostedDeployPackage> {
856
+ const absSourceDir = resolve(args.sourceDir);
857
+ ensureSourcePackageManifest(absSourceDir);
858
+
859
+ const absEntryPoint = resolveEntryPointPath(absSourceDir, args.entryPoint);
860
+ const tempRoot = mkdtempSync(join(tmpdir(), "libretto-deploy-"));
861
+ const outputDir = join(tempRoot, "deploy");
862
+ mkdirSync(outputDir, { recursive: true });
863
+ const librettoDependency = resolveLibrettoDependency(absSourceDir);
864
+
865
+ const additionalExternals = [...new Set(args.additionalExternals ?? [])];
866
+ // These packages stay out of the implementation bundle. The generated
867
+ // package.json carries them into deploy-time installation, and the deployed
868
+ // code resolves them from node_modules.
869
+ const externalPackages = new Set<string>([
870
+ ...DEFAULT_RUNTIME_EXTERNALS,
871
+ ...additionalExternals,
872
+ ]);
873
+ const workspacePackages = discoverWorkspacePackages(absSourceDir);
874
+ let callerOwnsTempRoot = false;
875
+
876
+ try {
877
+ await writeBundledDeployEntrypoint({
878
+ absEntryPoint,
879
+ absSourceDir,
880
+ deploymentName: args.deploymentName,
881
+ externalPackages,
882
+ outputDir,
883
+ workspacePackages,
884
+ });
885
+
886
+ if (librettoDependency === "file:./libretto") {
887
+ copyCurrentLibrettoPackage(outputDir);
888
+ }
889
+
890
+ // The generated manifest lists only packages that stay outside the
891
+ // implementation bundle. Hosted deploy installs them into the deployed
892
+ // package, and the deployed code loads them from node_modules.
893
+ writeDeployManifest({
894
+ additionalExternals,
895
+ deploymentName: args.deploymentName,
896
+ librettoDependency,
897
+ outputDir,
898
+ sourceDir: absSourceDir,
899
+ });
900
+
901
+ // Success transfers ownership of the temp directory to the caller, who is
902
+ // responsible for invoking cleanup() after the tarball/upload step.
903
+ callerOwnsTempRoot = true;
904
+ return {
905
+ cleanup: () => {
906
+ rmSync(tempRoot, { force: true, recursive: true });
907
+ },
908
+ entryPoint: "index.js",
909
+ outputDir,
910
+ };
911
+ } finally {
912
+ // On any failure before we return, this function still owns the temp dir
913
+ // and must remove it to avoid leaking deploy workspaces in /tmp.
914
+ if (!callerOwnsTempRoot) {
915
+ rmSync(tempRoot, { force: true, recursive: true });
916
+ }
917
+ }
918
+ }
919
+
920
+ export async function buildHostedDeployTarball(
921
+ args: BuildHostedDeployTarballArgs,
922
+ ): Promise<{ entryPoint: string; source: string }> {
923
+ const deployPackage = await createHostedDeployPackage(args);
924
+
925
+ try {
926
+ const tarPath = join(dirname(deployPackage.outputDir), "source.tar.gz");
927
+ execFileSync("tar", ["czf", tarPath, "-C", deployPackage.outputDir, "."], {
928
+ stdio: "pipe",
929
+ });
930
+
931
+ return {
932
+ entryPoint: deployPackage.entryPoint,
933
+ source: readFileSync(tarPath).toString("base64"),
934
+ };
935
+ } finally {
936
+ deployPackage.cleanup();
937
+ }
938
+ }