@blastlauncher/engine 0.0.0-stage → 3.0.0-alpha.1

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/bundle.js ADDED
@@ -0,0 +1,620 @@
1
+ import { context, transform } from "esbuild";
2
+ import { execFileSync } from "node:child_process";
3
+ import { access, chmod, copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, symlink, writeFile, } from "node:fs/promises";
4
+ import { builtinModules } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
7
+ import { checkExtensionDependencies } from "./install-dependencies.js";
8
+ import { loadSharedDependencies, } from "./dependencies.js";
9
+ import { loadTsconfigAliases } from "./tsconfig-paths.js";
10
+ // Bundles run in Node even when Bun runs the host. Bun's built-in list also
11
+ // includes npm packages such as undici and ws, which must be bundled for Node.
12
+ const nodeBuiltins = new Set(process.versions.bun
13
+ ? JSON.parse(execFileSync("node", ["-p", "JSON.stringify(require('node:module').builtinModules)"], {
14
+ encoding: "utf8",
15
+ }))
16
+ : builtinModules);
17
+ function isNodeBuiltin(specifier) {
18
+ return (nodeBuiltins.has(specifier) ||
19
+ (specifier.startsWith("node:") && nodeBuiltins.has(specifier.slice(5))));
20
+ }
21
+ export class MissingDependencyError extends Error {
22
+ code = "MISSING_DEPENDENCY";
23
+ packages;
24
+ constructor(packages) {
25
+ const names = [...new Set(packages)].toSorted();
26
+ super(`Missing dependencies: ${names.join(", ")}. Run blast deps <extensionDir> to install declared dependencies, or install @blastlauncher/shared-deps for the shared fallback.`);
27
+ this.name = "MissingDependencyError";
28
+ this.packages = names;
29
+ }
30
+ }
31
+ export class NativeModuleError extends Error {
32
+ code = "NATIVE_MODULE";
33
+ modules;
34
+ constructor(modules) {
35
+ const names = [...new Set(modules)].toSorted();
36
+ super(`Native modules are not supported in Blast: ${names.join(", ")}`);
37
+ this.name = "NativeModuleError";
38
+ this.modules = names;
39
+ }
40
+ }
41
+ export class BundleTimeoutError extends Error {
42
+ code = "BUNDLE_TIMEOUT";
43
+ extension;
44
+ command;
45
+ timeoutMs;
46
+ constructor(extension, command, timeoutMs) {
47
+ super(`Bundling ${extension}/${command} timed out after ${timeoutMs} ms`);
48
+ this.name = "BundleTimeoutError";
49
+ this.extension = extension;
50
+ this.command = command;
51
+ this.timeoutMs = timeoutMs;
52
+ }
53
+ }
54
+ export class BundleContainmentError extends Error {
55
+ code = "BUNDLE_OUTSIDE_ROOT";
56
+ specifier;
57
+ constructor(specifier) {
58
+ super(`Import ${JSON.stringify(specifier)} resolves outside the extension and dependency roots`);
59
+ this.name = "BundleContainmentError";
60
+ this.specifier = specifier;
61
+ }
62
+ }
63
+ /** Stop awaiting host work when cancelled, without leaving an abort listener behind. */
64
+ async function abortable(work, signal) {
65
+ if (signal.aborted)
66
+ return Promise.race([work, Promise.reject(signal.reason)]);
67
+ let abort;
68
+ try {
69
+ return await Promise.race([
70
+ work,
71
+ new Promise((_, reject) => {
72
+ abort = () => reject(signal.reason);
73
+ signal.addEventListener("abort", abort, { once: true });
74
+ }),
75
+ ]);
76
+ }
77
+ finally {
78
+ if (abort)
79
+ signal.removeEventListener("abort", abort);
80
+ }
81
+ }
82
+ function contained(root, path) {
83
+ const remainder = relative(root, path);
84
+ return remainder === "" || (!remainder.startsWith("..") && !isAbsolute(remainder));
85
+ }
86
+ async function packageRoot(path) {
87
+ const fallback = dirname(path);
88
+ let current = fallback;
89
+ while (true) {
90
+ if (await access(join(current, "package.json")).then(() => true, () => false))
91
+ return realpath(current);
92
+ const parent = dirname(current);
93
+ if (parent === current)
94
+ return fallback;
95
+ current = parent;
96
+ }
97
+ }
98
+ class BundleContainment {
99
+ roots = new Set();
100
+ sharedNodeModules = new Set();
101
+ sharedStores = new Set();
102
+ constructor(extensionDir, installed) {
103
+ this.roots.add(extensionDir);
104
+ if (installed)
105
+ this.roots.add(installed);
106
+ }
107
+ async registerInstalled(directory) {
108
+ const modules = join(directory, "node_modules");
109
+ const entries = await readdir(modules, { withFileTypes: true }).catch(() => []);
110
+ for (const entry of entries) {
111
+ const candidates = entry.name.startsWith("@")
112
+ ? await readdir(join(modules, entry.name), { withFileTypes: true }).catch(() => [])
113
+ : [entry];
114
+ for (const candidate of candidates) {
115
+ const path = join(modules, entry.name, entry.name.startsWith("@") ? candidate.name : "");
116
+ const target = await realpath(path).catch(() => undefined);
117
+ if (target)
118
+ this.roots.add(target);
119
+ }
120
+ }
121
+ }
122
+ async registerShared(path) {
123
+ // Shared resolvers may return extensionless package subpaths (for example
124
+ // lodash/get) that esbuild completes during its own resolution step.
125
+ const resolved = await realpath(path).catch(() => resolve(path));
126
+ const root = await packageRoot(resolved);
127
+ this.roots.add(root);
128
+ let current = root;
129
+ while (current !== dirname(current)) {
130
+ if (current.endsWith("/node_modules")) {
131
+ this.sharedNodeModules.add(current);
132
+ }
133
+ if (current.endsWith("/.bun"))
134
+ this.sharedStores.add(current);
135
+ current = dirname(current);
136
+ }
137
+ }
138
+ async assert(path, specifier) {
139
+ const resolved = await realpath(path);
140
+ if ([...this.roots, ...this.sharedNodeModules, ...this.sharedStores].some((root) => contained(root, resolved)))
141
+ return;
142
+ throw new BundleContainmentError(specifier);
143
+ }
144
+ }
145
+ function dependencyPolicy(missing, nativeModules, shared, installed, installedPackages = new Set(), aliases, containment) {
146
+ const dependencyFiles = new Set();
147
+ return {
148
+ name: "blast-dependencies",
149
+ setup(builder) {
150
+ builder.onResolve({ filter: /.*/ }, async (args) => {
151
+ if (args.pluginData === "blast-resolving")
152
+ return;
153
+ const specifier = args.path;
154
+ if (specifier === "@raycast/api" || /^react(?:\/|$)/.test(specifier))
155
+ return { path: specifier, external: true };
156
+ if (isNodeBuiltin(specifier))
157
+ return { path: specifier, external: true };
158
+ const bare = !specifier.startsWith(".") && !isAbsolute(specifier);
159
+ if (bare && /^(?:swift|rust):/.test(specifier)) {
160
+ nativeModules.add(specifier);
161
+ return { errors: [{ text: `Blast does not provide native module ${specifier}` }] };
162
+ }
163
+ // Relative and absolute imports are resolved here before esbuild reads
164
+ // them. This keeps an extension from smuggling an arbitrary file into
165
+ // the bundle through a default loader (including data/text loaders).
166
+ if (!bare) {
167
+ const result = await builder.resolve(specifier, {
168
+ resolveDir: args.resolveDir,
169
+ kind: args.kind,
170
+ pluginData: "blast-resolving",
171
+ });
172
+ if (!result.errors.length && result.path && !result.external) {
173
+ await containment?.assert(result.path, specifier);
174
+ if (dependencyFiles.has(args.importer))
175
+ dependencyFiles.add(result.path);
176
+ return result;
177
+ }
178
+ return;
179
+ }
180
+ const inDependencyGraph = dependencyFiles.has(args.importer);
181
+ // The extension's tsconfig `paths`/`baseUrl` win over packages, as
182
+ // tsc resolves them: a specifier that maps into the extension is the
183
+ // extension's own module, never a missing dependency. Imports from
184
+ // dependency graphs keep package resolution.
185
+ if (aliases && bare && !inDependencyGraph && args.kind !== "entry-point") {
186
+ const alias = await aliases.resolve(specifier);
187
+ if (alias) {
188
+ await containment?.assert(alias, specifier);
189
+ return { path: alias };
190
+ }
191
+ }
192
+ if (installed && bare && !inDependencyGraph && args.kind !== "entry-point") {
193
+ const name = specifier.startsWith("@")
194
+ ? specifier.split("/").slice(0, 2).join("/")
195
+ : specifier.split("/")[0];
196
+ // Do not let esbuild walk up into unrelated ancestor node_modules.
197
+ const present = await access(join(installed, "node_modules", name)).then(() => true, () => false);
198
+ if (present) {
199
+ const result = await builder.resolve(specifier, {
200
+ resolveDir: installed,
201
+ kind: args.kind,
202
+ pluginData: "blast-resolving",
203
+ });
204
+ if (!result.errors.length && result.path) {
205
+ if (!result.external)
206
+ await containment?.assert(result.path, specifier);
207
+ if (!result.external)
208
+ dependencyFiles.add(result.path);
209
+ installedPackages.add(name);
210
+ return result;
211
+ }
212
+ }
213
+ }
214
+ const provided = !inDependencyGraph && bare
215
+ ? shared?.resolveShared(specifier, args.kind === "require-call" || args.kind === "require-resolve"
216
+ ? "require"
217
+ : "import")
218
+ : undefined;
219
+ // Installed and shared dependency graphs get normal transitive resolution.
220
+ if (provided || inDependencyGraph) {
221
+ if (provided)
222
+ await containment?.registerShared(provided);
223
+ const result = await builder.resolve(provided ?? specifier, {
224
+ resolveDir: args.resolveDir,
225
+ kind: args.kind,
226
+ pluginData: "blast-resolving",
227
+ });
228
+ // Let esbuild handle unresolved optional requires inside try/catch.
229
+ // Returning plugin errors here would make those optional imports fatal.
230
+ if (result.errors.length && inDependencyGraph)
231
+ return;
232
+ if (result.path && !result.external) {
233
+ await containment?.assert(result.path, specifier);
234
+ dependencyFiles.add(result.path);
235
+ }
236
+ return result;
237
+ }
238
+ if (args.kind === "entry-point" || specifier.startsWith(".") || isAbsolute(specifier))
239
+ return;
240
+ const name = specifier.startsWith("@")
241
+ ? specifier.split("/").slice(0, 2).join("/")
242
+ : specifier.split("/")[0];
243
+ // `#imports` and other Node subpath aliases are not npm packages;
244
+ // report the bundler's ordinary unresolved-import error instead of a
245
+ // misleading "missing dependencies: #" result.
246
+ if (specifier.startsWith("#"))
247
+ return;
248
+ missing.add(name);
249
+ return { errors: [{ text: `Blast does not provide dependency ${name}` }] };
250
+ });
251
+ },
252
+ };
253
+ }
254
+ /**
255
+ * Raycast bundles extensions as CommonJS, so extensions freely use
256
+ * `__filename`, `__dirname` and `require`. Blast emits ESM, so the bundle
257
+ * banner defines the globals instead: paths derive from `import.meta.url` and
258
+ * `require` is `createRequire` restricted to Node built-ins and the
259
+ * runtime-supplied externals (`@raycast/api`, `react`, `react/*`), which the
260
+ * runtime's resolve hook maps for require() too: CommonJS packages such as
261
+ * `@raycast/utils` 1.x require React. Anything else is a missing dependency
262
+ * (docs/decisions/0002), not a silent resolution from the host filesystem.
263
+ */
264
+ const CJS_GLOBALS_BANNER = `
265
+ import { createRequire as __blastCreateRequire, isBuiltin as __blastIsBuiltin } from "node:module";
266
+ import { dirname as __blastDirname } from "node:path";
267
+ import { fileURLToPath as __blastFileURLToPath } from "node:url";
268
+ const __filename = __blastFileURLToPath(import.meta.url);
269
+ const __dirname = __blastDirname(__blastFileURLToPath(import.meta.url));
270
+ const __blastNodeRequire = __blastCreateRequire(import.meta.url);
271
+ function require(specifier) {
272
+ if (!__blastIsBuiltin(specifier) && specifier !== "@raycast/api" && !/^react(?:\\/|$)/.test(specifier)) {
273
+ const error = new Error(
274
+ 'Cannot require "' + specifier + '": Blast only provides Node built-ins to require() (MISSING_DEPENDENCY)',
275
+ );
276
+ error.code = "MISSING_DEPENDENCY";
277
+ throw error;
278
+ }
279
+ return __blastNodeRequire(specifier);
280
+ }
281
+ `.trim();
282
+ /** The preference types Raycast's manifest schema defines. */
283
+ const PREFERENCE_TYPES = [
284
+ "appPicker",
285
+ "checkbox",
286
+ "dropdown",
287
+ "password",
288
+ "textfield",
289
+ "file",
290
+ "directory",
291
+ ];
292
+ /** Shared manifest parsing for bundling defaults and client preference forms. */
293
+ export function parsePreferences(value, at = "Preferences") {
294
+ if (value === undefined)
295
+ return [];
296
+ if (!Array.isArray(value))
297
+ throw new Error(`${at} must be an array`);
298
+ return value.map((item, index) => {
299
+ if (typeof item !== "object" || item === null || Array.isArray(item))
300
+ throw new Error(`${at} entry ${index + 1} must be an object`);
301
+ const { name, type, default: fallback, description } = item;
302
+ if (typeof name !== "string" || !name)
303
+ throw new Error(`${at} entry ${index + 1} needs a name`);
304
+ if (typeof type !== "string" || !PREFERENCE_TYPES.includes(type))
305
+ throw new Error(`Preference ${JSON.stringify(name)} has an unknown type ${JSON.stringify(type)}`);
306
+ if (fallback !== undefined &&
307
+ typeof fallback !== "string" &&
308
+ typeof fallback !== "boolean" &&
309
+ !(typeof fallback === "number" && Number.isFinite(fallback)))
310
+ throw new Error(`Preference ${JSON.stringify(name)} needs a scalar "default"`);
311
+ if (description !== undefined && typeof description !== "string")
312
+ throw new Error(`Preference ${JSON.stringify(name)} needs a string "description"`);
313
+ return {
314
+ name,
315
+ type,
316
+ title: (type === "checkbox" ? item.label : item.title) || name,
317
+ required: item.required === true,
318
+ data: Array.isArray(item.data)
319
+ ? item.data.filter((option) => typeof option.value === "string" && typeof option.title === "string")
320
+ : [],
321
+ ...(typeof description === "string" ? { description } : {}),
322
+ ...(fallback === undefined ? {} : { default: fallback }),
323
+ };
324
+ });
325
+ }
326
+ function parsePreferenceDefaults(value, at) {
327
+ return Object.fromEntries(parsePreferences(value, at)
328
+ .filter((p) => p.default !== undefined)
329
+ .map((p) => [p.name, p.default]));
330
+ }
331
+ /** The argument types Raycast's manifest schema defines. */
332
+ const ARGUMENT_TYPES = ["text", "password", "dropdown"];
333
+ /**
334
+ * Reads `commands[].arguments`. Unknown per-argument fields (`data` on a
335
+ * dropdown) are ignored: Raycast's manifest is the extension author's, and new
336
+ * optional fields must not make an existing extension unloadable.
337
+ */
338
+ function parseCommandArguments(value) {
339
+ if (value === undefined)
340
+ return [];
341
+ if (!Array.isArray(value))
342
+ throw new Error("Command arguments must be an array");
343
+ const names = new Set();
344
+ return value.map((item, index) => {
345
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
346
+ throw new Error(`Argument ${index + 1} must be an object`);
347
+ }
348
+ const declared = item;
349
+ const name = declared["name"];
350
+ if (typeof name !== "string" || name === "")
351
+ throw new Error(`Argument ${index + 1} needs a name`);
352
+ if (names.has(name))
353
+ throw new Error(`Duplicate argument name ${JSON.stringify(name)}`);
354
+ names.add(name);
355
+ const type = declared["type"];
356
+ if (typeof type !== "string" || !ARGUMENT_TYPES.includes(type)) {
357
+ throw new Error(`Argument ${JSON.stringify(name)} has an unknown type ${JSON.stringify(type)}`);
358
+ }
359
+ const argument = { name, type };
360
+ const required = declared["required"];
361
+ if (required !== undefined) {
362
+ if (typeof required !== "boolean")
363
+ throw new Error(`Argument ${JSON.stringify(name)} needs a boolean "required"`);
364
+ argument.required = required;
365
+ }
366
+ const placeholder = declared["placeholder"];
367
+ if (placeholder !== undefined) {
368
+ if (typeof placeholder !== "string")
369
+ throw new Error(`Argument ${JSON.stringify(name)} needs a string "placeholder"`);
370
+ argument.placeholder = placeholder;
371
+ }
372
+ return argument;
373
+ });
374
+ }
375
+ /** Cap for the copy fallback, so a large assets directory cannot fill the disk. */
376
+ const MAX_COPIED_ASSETS_BYTES = 64 * 1024 * 1024;
377
+ async function totalAssetsSize(dir) {
378
+ let total = 0;
379
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
380
+ // A link inside assets could point anywhere; the copy fallback reads files only.
381
+ if (entry.isSymbolicLink())
382
+ continue;
383
+ const path = join(dir, entry.name);
384
+ if (entry.isDirectory())
385
+ total += await totalAssetsSize(path);
386
+ else if (entry.isFile())
387
+ total += (await stat(path)).size;
388
+ }
389
+ return total;
390
+ }
391
+ async function copyAssets(source, target) {
392
+ const size = await totalAssetsSize(source);
393
+ if (size > MAX_COPIED_ASSETS_BYTES) {
394
+ throw new Error(`Assets are ${size} bytes; the copy fallback caps at ${MAX_COPIED_ASSETS_BYTES}`);
395
+ }
396
+ await mkdir(target, { recursive: true });
397
+ for (const entry of await readdir(source, { withFileTypes: true })) {
398
+ if (entry.isSymbolicLink())
399
+ continue;
400
+ const from = join(source, entry.name);
401
+ const to = join(target, entry.name);
402
+ if (entry.isDirectory())
403
+ await copyAssets(from, to);
404
+ else if (entry.isFile()) {
405
+ await copyFile(from, to);
406
+ // Read-only: the copy stands in for assets the extension owns, not
407
+ // something the bundled command may rewrite.
408
+ await chmod(to, 0o444);
409
+ }
410
+ }
411
+ }
412
+ /**
413
+ * The build directory mirrors Raycast's layout: `assets/` sits next to the
414
+ * bundle, so `environment.assetsPath` and `path.join(__dirname, "assets", …)`
415
+ * both resolve to the extension's real assets. The link is a symlink; where
416
+ * the platform refuses symlinks, a read-only copy with a size cap is used.
417
+ * Nothing is ever written to the extension directory.
418
+ */
419
+ export async function linkAssets(outDir, extensionDir, link = symlink) {
420
+ const assets = join(extensionDir, "assets");
421
+ const source = await stat(assets).catch(() => undefined);
422
+ if (!source?.isDirectory())
423
+ return;
424
+ const target = join(outDir, "assets");
425
+ // `rm` removes a symlink itself, never its target, so re-bundling into a
426
+ // used output directory cannot touch the extension.
427
+ await rm(target, { recursive: true, force: true });
428
+ try {
429
+ await link(assets, target, "dir");
430
+ }
431
+ catch {
432
+ await copyAssets(assets, target);
433
+ }
434
+ }
435
+ /** The caller owns the output directory and may remove it once the session exits. */
436
+ async function bundleEntry(options) {
437
+ const started = performance.now();
438
+ const timeoutMs = options.timeoutMs ?? 60_000;
439
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 2_147_483_647)
440
+ throw new RangeError("timeoutMs must be an integer between 0 and 2147483647");
441
+ const controller = new AbortController();
442
+ const abort = () => controller.abort(options.signal?.reason);
443
+ options.signal?.addEventListener("abort", abort, { once: true });
444
+ if (options.signal?.aborted)
445
+ abort();
446
+ let extension = options.extensionDir;
447
+ const timer = timeoutMs === 0
448
+ ? undefined
449
+ : setTimeout(() => {
450
+ controller.abort(new BundleTimeoutError(extension, options.command, timeoutMs));
451
+ }, timeoutMs);
452
+ try {
453
+ const bundled = await bundleEntryWithSignal(options, controller.signal, (name) => {
454
+ extension = name;
455
+ });
456
+ return { ...bundled, bundleMs: performance.now() - started };
457
+ }
458
+ catch (error) {
459
+ if (error instanceof BundleTimeoutError)
460
+ throw new BundleTimeoutError(extension, options.command, timeoutMs);
461
+ throw error;
462
+ }
463
+ finally {
464
+ clearTimeout(timer);
465
+ options.signal?.removeEventListener("abort", abort);
466
+ }
467
+ }
468
+ async function bundleEntryWithSignal(options, signal, setExtension) {
469
+ signal.throwIfAborted();
470
+ const extensionDir = resolve(options.extensionDir);
471
+ const manifest = JSON.parse(await readFile(join(extensionDir, "package.json"), "utf8"));
472
+ const command = options.modelProvider
473
+ ? { name: manifest.ai?.modelProvider, mode: "model-provider" }
474
+ : manifest.commands?.find((item) => item.name === options.command);
475
+ if (typeof manifest.name !== "string" || !command)
476
+ throw new Error("Unknown extension command");
477
+ setExtension(manifest.name);
478
+ signal.throwIfAborted();
479
+ const entryName = options.modelProvider ? command.name : options.command;
480
+ if (typeof entryName !== "string" || !/^[\w-]+$/.test(entryName))
481
+ throw new Error("Invalid command name");
482
+ if (!options.modelProvider && !["view", "no-view", "menu-bar"].includes(command.mode))
483
+ throw new Error("Invalid command mode");
484
+ const commandArguments = parseCommandArguments(command.arguments);
485
+ let source;
486
+ for (const suffix of ["tsx", "ts", "jsx", "js"]) {
487
+ const candidate = join(extensionDir, "src", `${entryName}.${suffix}`);
488
+ try {
489
+ await access(candidate);
490
+ source = candidate;
491
+ break;
492
+ }
493
+ catch {
494
+ // Try the next supported source extension.
495
+ }
496
+ }
497
+ if (!source)
498
+ throw new Error(`Missing source for ${entryName}: no src/${entryName}.{tsx,ts,jsx,js} in ${extensionDir}` +
499
+ " (a sparse checkout that only has package.json? fetch the whole extension directory)");
500
+ const shared = await abortable(loadSharedDependencies(options.sharedDependencies), signal);
501
+ const aliases = await loadTsconfigAliases(extensionDir);
502
+ for (const warning of aliases?.warnings ?? [])
503
+ console.warn(warning);
504
+ const install = options.installedDependencies === false ? undefined : await checkExtensionDependencies(options);
505
+ const base = join(tmpdir(), "blast", "builds");
506
+ if (!options.outDir)
507
+ await mkdir(base, { recursive: true });
508
+ const outDir = options.outDir ? resolve(options.outDir) : await mkdtemp(join(base, "command-"));
509
+ const entry = join(outDir, `${entryName}.mjs`);
510
+ const missing = new Set();
511
+ const nativeModules = new Set();
512
+ const installedPackages = new Set();
513
+ const containment = new BundleContainment(await realpath(extensionDir), install && !install.needed ? await realpath(install.directory) : undefined);
514
+ if (install && !install.needed)
515
+ await containment.registerInstalled(install.directory);
516
+ try {
517
+ signal.throwIfAborted();
518
+ const builder = await context({
519
+ absWorkingDir: extensionDir,
520
+ entryPoints: [source],
521
+ outfile: entry,
522
+ bundle: true,
523
+ metafile: true,
524
+ write: false,
525
+ format: "esm",
526
+ platform: "node",
527
+ target: "node24",
528
+ jsx: "automatic",
529
+ banner: { js: CJS_GLOBALS_BANNER },
530
+ plugins: [
531
+ dependencyPolicy(missing, nativeModules, shared, install && !install.needed ? install.directory : undefined, installedPackages, aliases, containment),
532
+ ],
533
+ logLevel: "silent",
534
+ });
535
+ let bundled;
536
+ try {
537
+ signal.throwIfAborted();
538
+ bundled = await abortable(builder.rebuild(), signal);
539
+ }
540
+ finally {
541
+ // cancel stops esbuild's graph walk; dispose releases the service context.
542
+ if (signal.aborted)
543
+ await builder.cancel();
544
+ await builder.dispose();
545
+ }
546
+ signal.throwIfAborted();
547
+ // Optional imports in provided graphs can escape esbuild's resolver.
548
+ // Optional require calls use the banner's MISSING_DEPENDENCY guard instead.
549
+ for (const output of Object.values(bundled.metafile.outputs)) {
550
+ for (const imported of output.imports) {
551
+ if (imported.external &&
552
+ imported.kind !== "require-call" &&
553
+ imported.path !== "@raycast/api" &&
554
+ !/^react(?:\/|$)/.test(imported.path) &&
555
+ !isNodeBuiltin(imported.path)) {
556
+ const name = imported.path.startsWith("@")
557
+ ? imported.path.split("/").slice(0, 2).join("/")
558
+ : imported.path.split("/")[0];
559
+ if (!imported.path.startsWith("#"))
560
+ missing.add(name);
561
+ }
562
+ }
563
+ }
564
+ if (nativeModules.size)
565
+ throw new NativeModuleError(nativeModules);
566
+ if (missing.size)
567
+ throw new MissingDependencyError(missing);
568
+ // After tree shaking utils' monolithic entry, remove unused named imports
569
+ // so Node does not try to link shim symbols the command never uses.
570
+ const output = await transform(bundled.outputFiles[0].text, {
571
+ format: "esm",
572
+ target: "node24",
573
+ minifySyntax: true,
574
+ });
575
+ signal.throwIfAborted();
576
+ await mkdir(outDir, { recursive: true });
577
+ await writeFile(entry, output.code);
578
+ await linkAssets(outDir, extensionDir);
579
+ signal.throwIfAborted();
580
+ }
581
+ catch (error) {
582
+ if (!options.outDir)
583
+ await rm(outDir, { recursive: true, force: true });
584
+ signal.throwIfAborted();
585
+ if (nativeModules.size)
586
+ throw new NativeModuleError(nativeModules);
587
+ if (missing.size)
588
+ throw new MissingDependencyError(missing);
589
+ throw error;
590
+ }
591
+ return {
592
+ entry,
593
+ dependencyWarnings: shared?.dependencyWarnings(Object.fromEntries(Object.entries(manifest.dependencies ?? {}).filter(([name]) => !installedPackages.has(name)))) ?? [],
594
+ extensionName: manifest.name,
595
+ commandName: command.name,
596
+ // Raycast reports the owner when a manifest has one, else the author.
597
+ ownerOrAuthorName: typeof manifest.owner === "string"
598
+ ? manifest.owner
599
+ : typeof manifest.author === "string"
600
+ ? manifest.author
601
+ : "",
602
+ mode: command.mode,
603
+ arguments: commandArguments,
604
+ // Command preferences shadow extension preferences with the same name.
605
+ preferenceDefaults: {
606
+ ...parsePreferenceDefaults(manifest.preferences, "Extension preferences"),
607
+ ...parsePreferenceDefaults(command.preferences, `Command ${command.name} preferences`),
608
+ },
609
+ assetsPath: join(outDir, "assets"),
610
+ ...(manifest.ai?.modelProvider && !options.modelProvider
611
+ ? { modelProviderDir: extensionDir }
612
+ : {}),
613
+ };
614
+ }
615
+ export function bundleModelProvider(options) {
616
+ return bundleEntry({ ...options, command: "model-provider", modelProvider: true });
617
+ }
618
+ export function bundleCommand(options) {
619
+ return bundleEntry(options);
620
+ }
@@ -0,0 +1,6 @@
1
+ import { type CapabilityHandler } from "./broker.js";
2
+ import { type StorageOptions } from "./storage.js";
3
+ /** Session-owned handlers always win over client grants and defaults. */
4
+ export declare function createSessionBroker(storage: StorageOptions, options?: {
5
+ handlers?: Record<string, CapabilityHandler>;
6
+ }): import("./broker.js").Broker;
package/dist/client.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createBroker } from "./broker.js";
2
+ import { createStorageHandlers } from "./storage.js";
3
+ import { createOAuthHandlers } from "./oauth.js";
4
+ /** Session-owned handlers always win over client grants and defaults. */
5
+ export function createSessionBroker(storage, options = {}) {
6
+ return createBroker({
7
+ ...options.handlers,
8
+ ...createStorageHandlers(storage),
9
+ ...createOAuthHandlers(storage),
10
+ });
11
+ }
@@ -0,0 +1,7 @@
1
+ /** Optional fallback supplied by @blastlauncher/shared-deps. */
2
+ export type SharedDependencies = {
3
+ resolveShared(specifier: string, kind?: "import" | "require"): string | undefined;
4
+ dependencyWarnings(declarations?: Record<string, unknown>): string[];
5
+ };
6
+ export type SharedDependencyLoader = () => Promise<SharedDependencies>;
7
+ export declare function loadSharedDependencies(load?: SharedDependencyLoader): Promise<SharedDependencies | undefined>;
@@ -0,0 +1,11 @@
1
+ export async function loadSharedDependencies(load = () => import("@blastlauncher/shared-deps")) {
2
+ try {
3
+ return await load();
4
+ }
5
+ catch (error) {
6
+ if (error.code === "ERR_MODULE_NOT_FOUND" &&
7
+ /Cannot find (?:package|module) ['"]@blastlauncher\/shared-deps['"]/.test(error.message))
8
+ return undefined;
9
+ throw error;
10
+ }
11
+ }