@aexol/spectral 0.9.103 → 0.9.105

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.
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAyBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAWlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqdlF;AA+DD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAyBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAWlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwdlF;AA+DD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
@@ -332,6 +332,7 @@ export async function runServe(opts = {}) {
332
332
  logger: silent
333
333
  ? { log: () => { }, warn: () => { }, error: () => { } }
334
334
  : console,
335
+ runtimeInfo: { version, pid: process.pid },
335
336
  });
336
337
  wireRelay(relay);
337
338
  relay.connect();
@@ -402,6 +403,7 @@ export async function runServe(opts = {}) {
402
403
  logger: silent
403
404
  ? { log: () => { }, warn: () => { }, error: () => { } }
404
405
  : console,
406
+ runtimeInfo: { version, pid: process.pid },
405
407
  });
406
408
  wireRelay(relay);
407
409
  relay.connect();
@@ -426,6 +428,7 @@ export async function runServe(opts = {}) {
426
428
  webSocketImpl: opts.webSocketImpl,
427
429
  fetchImpl: opts.fetchImpl,
428
430
  logger: silent ? { log: () => { }, warn: () => { }, error: () => { } } : console,
431
+ runtimeInfo: { version, pid: process.pid },
429
432
  });
430
433
  wireRelay(relay);
431
434
  relay.connect();
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/generate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAa,WAAW,EAA6B,MAAM,aAAa,CAAC;AAiBhG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAkH/E;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAO7E"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/generate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EACV,SAAS,EAET,WAAW,EAGZ,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,SAAS,GAChB,IAAI,CA2KN;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAO7E"}
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { createDocumentPaths, ensureDir, pathToFileHref, toDisplayPath } from "../utils/paths.js";
2
+ import { createDocumentPaths, ensureDir, pathToFileHref, toDisplayPath, } from "../utils/paths.js";
3
3
  import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
4
4
  import { renderPdf } from "../utils/renderer.js";
5
5
  import { errorResult, textResult } from "./helpers.js";
@@ -29,15 +29,45 @@ export function registerGenerateTool(ext, config) {
29
29
  type: "object",
30
30
  properties: {
31
31
  title: { type: "string", description: "Document title." },
32
- html: { type: "string", description: "Complete HTML document. Must include doctype, html, head, and body." },
33
- outputName: { type: "string", description: "Optional stable filename/folder slug." },
34
- format: { type: "string", enum: [...formatEnum], description: "Page format. Default from config: A4." },
35
- orientation: { type: "string", enum: [...orientationEnum], description: "Page orientation. Default: portrait." },
36
- printBackground: { type: "boolean", description: "Render CSS backgrounds. Default: true." },
37
- allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation/render. Default: false." },
38
- viewport: { type: "object", properties: { width: { type: "number" }, height: { type: "number" } }, description: "Override the browser viewport before rendering (useful for slide decks). Default: auto." },
39
- width: { type: "number", description: "Override PDF page width in pixels. Default: auto from format." },
40
- height: { type: "number", description: "Override PDF page height in pixels. Default: auto from format." },
32
+ html: {
33
+ type: "string",
34
+ description: "Complete HTML document. Must include doctype, html, head, and body.",
35
+ },
36
+ outputName: {
37
+ type: "string",
38
+ description: "Optional stable filename/folder slug.",
39
+ },
40
+ format: {
41
+ type: "string",
42
+ enum: [...formatEnum],
43
+ description: "Page format. Default from config: A4.",
44
+ },
45
+ orientation: {
46
+ type: "string",
47
+ enum: [...orientationEnum],
48
+ description: "Page orientation. Default: portrait.",
49
+ },
50
+ printBackground: {
51
+ type: "boolean",
52
+ description: "Render CSS backgrounds. Default: true.",
53
+ },
54
+ allowRemoteAssets: {
55
+ type: "boolean",
56
+ description: "Allow http/https assets during validation/render. Default: false.",
57
+ },
58
+ viewport: {
59
+ type: "object",
60
+ properties: { width: { type: "number" }, height: { type: "number" } },
61
+ description: "Override the browser viewport before rendering (useful for slide decks). Default: auto.",
62
+ },
63
+ width: {
64
+ type: "number",
65
+ description: "Custom PDF page width in millimetres. Omit for format-based sizing.",
66
+ },
67
+ height: {
68
+ type: "number",
69
+ description: "Custom PDF page height in millimetres. Omit for format-based sizing.",
70
+ },
41
71
  margin: {
42
72
  type: "object",
43
73
  properties: {
@@ -69,7 +99,13 @@ export function registerGenerateTool(ext, config) {
69
99
  pdfPath: toDisplayPath(ctx.cwd, paths.pdfPath),
70
100
  createdAt: now,
71
101
  updatedAt: now,
72
- page: { format, orientation, margin },
102
+ page: {
103
+ format,
104
+ orientation,
105
+ width: p.width,
106
+ height: p.height,
107
+ margin,
108
+ },
73
109
  source: "generated",
74
110
  };
75
111
  writeManifest(paths.manifestPath, manifest);
@@ -86,7 +122,10 @@ export function registerGenerateTool(ext, config) {
86
122
  height: p.height,
87
123
  waitUntil: allowRemoteAssets ? "networkidle" : "load",
88
124
  });
89
- writeManifest(paths.manifestPath, { ...manifest, updatedAt: new Date().toISOString() });
125
+ writeManifest(paths.manifestPath, {
126
+ ...manifest,
127
+ updatedAt: new Date().toISOString(),
128
+ });
90
129
  const warning = printCssWarning(p.html);
91
130
  const lines = [
92
131
  "✓ PDF generated from editable HTML",
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA+C9E"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/preview.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAW7C,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,SAAS,GAChB,IAAI,CAuFN"}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
3
+ import { assertInsideWorkspace, ensureParent, resolveWorkspacePath, toDisplayPath, } from "../utils/paths.js";
4
4
  import { validateHtmlDocument } from "../utils/html.js";
5
5
  import { renderPreview } from "../utils/renderer.js";
6
6
  import { errorResult, textResult } from "./helpers.js";
@@ -13,9 +13,23 @@ export function registerPreviewTool(ext, config) {
13
13
  parameters: {
14
14
  type: "object",
15
15
  properties: {
16
- htmlPath: { type: "string", description: "Path to an HTML file inside the workspace." },
17
- outputPath: { type: "string", description: "Optional PNG output path inside the workspace." },
18
- allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during preview. Default: false." },
16
+ htmlPath: {
17
+ type: "string",
18
+ description: "Path to an HTML file inside the workspace.",
19
+ },
20
+ outputPath: {
21
+ type: "string",
22
+ description: "Optional PNG output path inside the workspace.",
23
+ },
24
+ orientation: {
25
+ type: "string",
26
+ enum: ["portrait", "landscape"],
27
+ description: "Preview orientation. Default from PDF config.",
28
+ },
29
+ allowRemoteAssets: {
30
+ type: "boolean",
31
+ description: "Allow http/https assets during preview. Default: false.",
32
+ },
19
33
  },
20
34
  required: ["htmlPath"],
21
35
  },
@@ -26,12 +40,16 @@ export function registerPreviewTool(ext, config) {
26
40
  assertInsideWorkspace(ctx.cwd, htmlPath);
27
41
  if (!existsSync(htmlPath))
28
42
  throw new Error(`HTML file not found: ${p.htmlPath}`);
29
- const previewPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : join(dirname(htmlPath), "preview.png");
43
+ const previewPath = p.outputPath
44
+ ? resolveWorkspacePath(ctx.cwd, p.outputPath)
45
+ : join(dirname(htmlPath), "preview.png");
30
46
  assertInsideWorkspace(ctx.cwd, previewPath);
31
47
  ensureParent(previewPath);
32
48
  const allowRemoteAssets = p.allowRemoteAssets ?? config.allowRemoteAssets;
33
- validateHtmlDocument(readFileSync(htmlPath, "utf-8"), { allowRemoteAssets });
34
- const preview = await renderPreview(config, htmlPath, previewPath, allowRemoteAssets);
49
+ validateHtmlDocument(readFileSync(htmlPath, "utf-8"), {
50
+ allowRemoteAssets,
51
+ });
52
+ const preview = await renderPreview(config, htmlPath, previewPath, allowRemoteAssets, p.orientation);
35
53
  return textResult([
36
54
  "✓ HTML preview rendered",
37
55
  `HTML: ${toDisplayPath(ctx.cwd, htmlPath)}`,
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoC,MAAM,oCAAoC,CAAC;AACzG,OAAO,KAAK,EAAE,SAAS,EAAqD,MAAM,aAAa,CAAC;AAgBhG,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAgH7E"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/tools/render.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EACV,SAAS,EAKV,MAAM,aAAa,CAAC;AAyBrB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAqK7E"}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, extname, join } from "node:path";
3
- import { assertInsideWorkspace, ensureParent, pathToFileHref, resolveWorkspacePath, toDisplayPath } from "../utils/paths.js";
3
+ import { assertInsideWorkspace, ensureParent, pathToFileHref, resolveWorkspacePath, toDisplayPath, } from "../utils/paths.js";
4
4
  import { printCssWarning, validateHtmlDocument } from "../utils/html.js";
5
5
  import { renderPdf } from "../utils/renderer.js";
6
6
  import { errorResult, textResult } from "./helpers.js";
@@ -25,15 +25,45 @@ export function registerRenderTool(ext, config) {
25
25
  parameters: {
26
26
  type: "object",
27
27
  properties: {
28
- htmlPath: { type: "string", description: "Path to the existing HTML file, relative to workspace or absolute inside workspace." },
29
- outputPath: { type: "string", description: "Optional PDF output path, relative to workspace or absolute inside workspace." },
30
- format: { type: "string", enum: ["A4", "Letter"], description: "Page format. Default: A4." },
31
- orientation: { type: "string", enum: ["portrait", "landscape"], description: "Page orientation. Default: portrait." },
32
- printBackground: { type: "boolean", description: "Render CSS backgrounds. Default: true." },
33
- allowRemoteAssets: { type: "boolean", description: "Allow http/https assets during validation/render. Default: false." },
34
- viewport: { type: "object", properties: { width: { type: "number" }, height: { type: "number" } }, description: "Override the browser viewport before rendering. Default: auto." },
35
- width: { type: "number", description: "Override PDF page width in pixels. Default: auto from format." },
36
- height: { type: "number", description: "Override PDF page height in pixels. Default: auto from format." },
28
+ htmlPath: {
29
+ type: "string",
30
+ description: "Path to the existing HTML file, relative to workspace or absolute inside workspace.",
31
+ },
32
+ outputPath: {
33
+ type: "string",
34
+ description: "Optional PDF output path, relative to workspace or absolute inside workspace.",
35
+ },
36
+ format: {
37
+ type: "string",
38
+ enum: ["A4", "Letter"],
39
+ description: "Page format. Default: A4.",
40
+ },
41
+ orientation: {
42
+ type: "string",
43
+ enum: ["portrait", "landscape"],
44
+ description: "Page orientation. Default: portrait.",
45
+ },
46
+ printBackground: {
47
+ type: "boolean",
48
+ description: "Render CSS backgrounds. Default: true.",
49
+ },
50
+ allowRemoteAssets: {
51
+ type: "boolean",
52
+ description: "Allow http/https assets during validation/render. Default: false.",
53
+ },
54
+ viewport: {
55
+ type: "object",
56
+ properties: { width: { type: "number" }, height: { type: "number" } },
57
+ description: "Override the browser viewport before rendering. Default: auto.",
58
+ },
59
+ width: {
60
+ type: "number",
61
+ description: "Custom PDF page width in millimetres. Omit for format-based sizing.",
62
+ },
63
+ height: {
64
+ type: "number",
65
+ description: "Custom PDF page height in millimetres. Omit for format-based sizing.",
66
+ },
37
67
  margin: {
38
68
  type: "object",
39
69
  properties: {
@@ -53,7 +83,9 @@ export function registerRenderTool(ext, config) {
53
83
  assertInsideWorkspace(ctx.cwd, htmlPath);
54
84
  if (!existsSync(htmlPath))
55
85
  throw new Error(`HTML file not found: ${p.htmlPath}`);
56
- const outputPath = p.outputPath ? resolveWorkspacePath(ctx.cwd, p.outputPath) : defaultPdfPath(htmlPath);
86
+ const outputPath = p.outputPath
87
+ ? resolveWorkspacePath(ctx.cwd, p.outputPath)
88
+ : defaultPdfPath(htmlPath);
57
89
  assertInsideWorkspace(ctx.cwd, outputPath);
58
90
  ensureParent(outputPath);
59
91
  const html = readFileSync(htmlPath, "utf-8");
@@ -84,7 +116,13 @@ export function registerRenderTool(ext, config) {
84
116
  pdfPath: toDisplayPath(ctx.cwd, outputPath),
85
117
  createdAt: now,
86
118
  updatedAt: now,
87
- page: { format, orientation, margin },
119
+ page: {
120
+ format,
121
+ orientation,
122
+ width: p.width,
123
+ height: p.height,
124
+ margin,
125
+ },
88
126
  source: "rendered",
89
127
  };
90
128
  const manifestPath = join(dirname(htmlPath), "manifest.json");
@@ -20,7 +20,7 @@ export interface RenderPdfResult {
20
20
  bytes: number;
21
21
  }
22
22
  export declare function renderPdf(config: PdfConfig, opts: RenderPdfOptions): Promise<RenderPdfResult>;
23
- export declare function renderPreview(config: PdfConfig, htmlPath: string, previewPath: string, allowRemoteAssets: boolean): Promise<{
23
+ export declare function renderPreview(config: PdfConfig, htmlPath: string, previewPath: string, allowRemoteAssets: boolean, orientation?: PdfOrientation): Promise<{
24
24
  previewPath: string;
25
25
  bytes: number;
26
26
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGnF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmCnG;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBzK"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,OAAO,EAC1B,WAAW,GAAE,cAA0C,GACtD,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCjD"}
@@ -2,6 +2,17 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { chromium } from "playwright";
3
3
  import { pathToFileHref } from "./paths.js";
4
4
  export async function renderPdf(config, opts) {
5
+ for (const [name, value] of [
6
+ ["width", opts.width],
7
+ ["height", opts.height],
8
+ ]) {
9
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
10
+ throw new Error(`Custom PDF ${name} must be a positive number of millimetres.`);
11
+ }
12
+ }
13
+ if ((opts.width === undefined) !== (opts.height === undefined)) {
14
+ throw new Error("Custom PDF width and height must be provided together.");
15
+ }
5
16
  const browser = await chromium.launch({ headless: true });
6
17
  try {
7
18
  const page = await browser.newPage();
@@ -12,7 +23,9 @@ export async function renderPdf(config, opts) {
12
23
  if (!opts.allowRemoteAssets) {
13
24
  await page.route("**/*", (route) => {
14
25
  const url = route.request().url();
15
- if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank") {
26
+ if (url.startsWith("file://") ||
27
+ url.startsWith("data:") ||
28
+ url === "about:blank") {
16
29
  return route.continue();
17
30
  }
18
31
  return route.abort();
@@ -27,8 +40,10 @@ export async function renderPdf(config, opts) {
27
40
  format: opts.format,
28
41
  landscape: opts.orientation === "landscape",
29
42
  printBackground: opts.printBackground,
30
- ...(opts.width ? { width: opts.width } : {}),
31
- ...(opts.height ? { height: opts.height } : {}),
43
+ // Chromium expects PDF dimensions as strings with units. Numeric values
44
+ // are CSS pixels and can accidentally make a paper-sized page tiny.
45
+ ...(opts.width !== undefined ? { width: `${opts.width}mm` } : {}),
46
+ ...(opts.height !== undefined ? { height: `${opts.height}mm` } : {}),
32
47
  margin: opts.margin,
33
48
  });
34
49
  if (!existsSync(opts.pdfPath))
@@ -39,20 +54,30 @@ export async function renderPdf(config, opts) {
39
54
  await browser.close();
40
55
  }
41
56
  }
42
- export async function renderPreview(config, htmlPath, previewPath, allowRemoteAssets) {
57
+ export async function renderPreview(config, htmlPath, previewPath, allowRemoteAssets, orientation = config.defaultOrientation) {
43
58
  const browser = await chromium.launch({ headless: true });
44
59
  try {
45
- const page = await browser.newPage({ viewport: { width: 1240, height: 1754 }, deviceScaleFactor: 1 });
60
+ // Keep the preview in the same aspect ratio as the configured paper. This is
61
+ // intentionally a CSS viewport, not a page.pdf width/height override.
62
+ const viewport = orientation === "landscape"
63
+ ? { width: 1754, height: 1240 }
64
+ : { width: 1240, height: 1754 };
65
+ const page = await browser.newPage({ viewport, deviceScaleFactor: 1 });
46
66
  page.setDefaultTimeout(config.renderTimeoutMs);
47
67
  if (!allowRemoteAssets) {
48
68
  await page.route("**/*", (route) => {
49
69
  const url = route.request().url();
50
- if (url.startsWith("file://") || url.startsWith("data:") || url === "about:blank")
70
+ if (url.startsWith("file://") ||
71
+ url.startsWith("data:") ||
72
+ url === "about:blank")
51
73
  return route.continue();
52
74
  return route.abort();
53
75
  });
54
76
  }
55
- await page.goto(pathToFileHref(htmlPath), { waitUntil: "load", timeout: config.renderTimeoutMs });
77
+ await page.goto(pathToFileHref(htmlPath), {
78
+ waitUntil: "load",
79
+ timeout: config.renderTimeoutMs,
80
+ });
56
81
  await page.screenshot({ path: previewPath, fullPage: true, type: "png" });
57
82
  return { previewPath, bytes: statSync(previewPath).size };
58
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/validate.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,kBAAkB,EAAE,CA8HzG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAE9E"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/pdf/utils/validate.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA2CD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACrC,kBAAkB,EAAE,CAkMtB;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAET"}
@@ -15,6 +15,23 @@ const WIDE_WIDTH_RE = /(?:width|min-width)\s*:\s*(?:[1-9]\d{3,}px|(?:8[5-9]|9\d|
15
15
  const IMAGE_RE = /<img\b[^>]*>/gi;
16
16
  const WIDTH_ATTR_RE = /\bwidth\s*=\s*["'][^"']+["']/i;
17
17
  const HEIGHT_ATTR_RE = /\bheight\s*=\s*["'][^"']+["']/i;
18
+ const TRANSFORM_SCALE_RE = /transform\s*:[^;{}]*scale\s*\(/i;
19
+ const ZOOM_RE = /zoom\s*:\s*(?!1(?:\.0+)?\b)(?:0?\.)?\d+/i;
20
+ const FIXED_VIEWPORT_RE = /(?:width|height)\s*:\s*100vw|(?:width|height)\s*:\s*100vh|(?:width|min-width|height|min-height)\s*:\s*\d{4,}px/i;
21
+ const PAGE_SIZE_RE = /@page\s*{[^}]*size\s*:\s*([^;}]*)/is;
22
+ function normalizePageSize(size) {
23
+ const value = size.trim().toLowerCase();
24
+ const orientation = value.includes("landscape")
25
+ ? "landscape"
26
+ : value.includes("portrait")
27
+ ? "portrait"
28
+ : undefined;
29
+ if (value.includes("a4"))
30
+ return { format: "A4", orientation };
31
+ if (value.includes("letter"))
32
+ return { format: "Letter", orientation };
33
+ return { orientation };
34
+ }
18
35
  export function analyzePdfHtml(html, opts) {
19
36
  const issues = [];
20
37
  if (html.length > 5_000_000) {
@@ -34,13 +51,25 @@ export function analyzePdfHtml(html, opts) {
34
51
  });
35
52
  }
36
53
  if (!HTML_RE.test(html)) {
37
- issues.push({ severity: "error", code: "missing_html", message: "HTML must include <html>." });
54
+ issues.push({
55
+ severity: "error",
56
+ code: "missing_html",
57
+ message: "HTML must include <html>.",
58
+ });
38
59
  }
39
60
  if (!HEAD_RE.test(html)) {
40
- issues.push({ severity: "error", code: "missing_head", message: "HTML must include <head>." });
61
+ issues.push({
62
+ severity: "error",
63
+ code: "missing_head",
64
+ message: "HTML must include <head>.",
65
+ });
41
66
  }
42
67
  if (!BODY_RE.test(html)) {
43
- issues.push({ severity: "error", code: "missing_body", message: "HTML must include <body>." });
68
+ issues.push({
69
+ severity: "error",
70
+ code: "missing_body",
71
+ message: "HTML must include <body>.",
72
+ });
44
73
  }
45
74
  if (!TITLE_RE.test(html)) {
46
75
  issues.push({
@@ -55,7 +84,7 @@ export function analyzePdfHtml(html, opts) {
55
84
  severity: "info",
56
85
  code: "missing_viewport",
57
86
  message: "HTML has no viewport meta tag.",
58
- suggestion: "Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">.",
87
+ suggestion: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.',
59
88
  });
60
89
  }
61
90
  if (!PAGE_RE.test(html)) {
@@ -123,6 +152,42 @@ export function analyzePdfHtml(html, opts) {
123
152
  suggestion: "Check pagination carefully; absolute elements can overlap page breaks.",
124
153
  });
125
154
  }
155
+ if (TRANSFORM_SCALE_RE.test(html)) {
156
+ issues.push({
157
+ severity: "warning",
158
+ code: "transform_scale",
159
+ message: "CSS uses transform: scale(), which can shrink or clip PDF content.",
160
+ suggestion: "Use print-native dimensions and layout instead of scaling the whole document.",
161
+ });
162
+ }
163
+ if (ZOOM_RE.test(html)) {
164
+ issues.push({
165
+ severity: "warning",
166
+ code: "css_zoom",
167
+ message: "CSS uses zoom other than 1, which can change PDF pagination and scale.",
168
+ suggestion: "Remove zoom for print output or scope it to an intentional print layout.",
169
+ });
170
+ }
171
+ if (FIXED_VIEWPORT_RE.test(html)) {
172
+ issues.push({
173
+ severity: "warning",
174
+ code: "fixed_or_viewport_layout",
175
+ message: "A viewport-sized or fixed oversized layout may not fit the selected paper.",
176
+ suggestion: "Use responsive width: 100% with print margins and avoid fixed pixel canvases.",
177
+ });
178
+ }
179
+ const pageSizeMatch = html.match(PAGE_SIZE_RE);
180
+ if (pageSizeMatch) {
181
+ const pageSize = normalizePageSize(pageSizeMatch[1]);
182
+ if (pageSize.orientation && pageSize.format) {
183
+ issues.push({
184
+ severity: "info",
185
+ code: "page_size_orientation",
186
+ message: `@page explicitly requests ${pageSize.format} ${pageSize.orientation}.`,
187
+ suggestion: "Keep @page size aligned with the PDF format and orientation options.",
188
+ });
189
+ }
190
+ }
126
191
  if (WIDE_WIDTH_RE.test(html)) {
127
192
  issues.push({
128
193
  severity: "warning",
@@ -66,6 +66,11 @@ export interface RelayClientOptions {
66
66
  * Optional logger. Defaults to console. Tests pass a noop.
67
67
  */
68
68
  logger?: Pick<Console, "log" | "warn" | "error">;
69
+ /** Runtime metadata sent after every welcome/reconnect. */
70
+ runtimeInfo?: {
71
+ version: string;
72
+ pid: number;
73
+ };
69
74
  /**
70
75
  * Override `process.exit` for tests. Called when the backend evicts us
71
76
  * with `replaced-by-newer-registration` to avoid a reconnect ping-pong.
@@ -87,6 +92,7 @@ export declare class RelayClient extends EventEmitter {
87
92
  private readonly fetchImpl;
88
93
  private readonly logger;
89
94
  private readonly exit;
95
+ private readonly runtimeInfo;
90
96
  private ws;
91
97
  private disposed;
92
98
  private reconnectAttempt;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/relay/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,SAAS,MAAM,IAAI,CAAC;AAqB3B,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,SAAS,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACjD;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAE/C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,SAAS,CAAgB;IACjC,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAS;IAE3B,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,aAAa,CAA+B;gBAExC,IAAI,EAAE,kBAAkB;IAWpC,kEAAkE;IAClE,OAAO,IAAI,IAAI;IAMf;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO;IAqBzC;;;OAGG;IACH,OAAO,IAAI,IAAI;IA2Bf,OAAO,CAAC,UAAU;IAgJlB,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;YACW,WAAW;YA8BX,iBAAiB;CAmDhC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/relay/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,SAAS,MAAM,IAAI,CAAC;AAqB3B,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,SAAS,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACjD,2DAA2D;IAC3D,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+C;IAE3E,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,SAAS,CAAgB;IACjC,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAS;IAE3B,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,aAAa,CAA+B;gBAExC,IAAI,EAAE,kBAAkB;IAYpC,kEAAkE;IAClE,OAAO,IAAI,IAAI;IAMf;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO;IAqBzC;;;OAGG;IACH,OAAO,IAAI,IAAI;IA2Bf,OAAO,CAAC,UAAU;IAmJlB,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;YACW,WAAW;YA8BX,iBAAiB;CAmDhC"}
@@ -67,6 +67,7 @@ export class RelayClient extends EventEmitter {
67
67
  fetchImpl;
68
68
  logger;
69
69
  exit;
70
+ runtimeInfo;
70
71
  ws = null;
71
72
  disposed = false;
72
73
  reconnectAttempt = 0;
@@ -86,6 +87,7 @@ export class RelayClient extends EventEmitter {
86
87
  this.fetchImpl = opts.fetchImpl ?? fetch;
87
88
  this.logger = opts.logger ?? console;
88
89
  this.exit = opts.exit ?? ((code) => process.exit(code));
90
+ this.runtimeInfo = opts.runtimeInfo;
89
91
  }
90
92
  /** Open the connection. Idempotent — calling twice is a no-op. */
91
93
  connect() {
@@ -208,6 +210,9 @@ export class RelayClient extends EventEmitter {
208
210
  }
209
211
  if (parsed && parsed.kind === "welcome") {
210
212
  this.emit("welcome", parsed);
213
+ if (this.runtimeInfo) {
214
+ this.send({ kind: "runtime_info", ...this.runtimeInfo });
215
+ }
211
216
  return;
212
217
  }
213
218
  this.emit("frame", parsed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.103",
3
+ "version": "0.9.105",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,