@open-pencil/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-pencil/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,12 +19,12 @@
19
19
  "provenance": true
20
20
  },
21
21
  "dependencies": {
22
+ "@open-pencil/core": "^0.9.0",
22
23
  "agentfmt": "^0.1.3",
23
24
  "canvaskit-wasm": "^0.40.0",
24
- "citty": "^0.1.6",
25
- "@open-pencil/core": "0.8.0"
25
+ "citty": "^0.1.6"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/bun": "^1.2.9"
29
29
  }
30
- }
30
+ }
@@ -9,7 +9,7 @@ import type { AnalyzeClustersResult } from '@open-pencil/core'
9
9
 
10
10
  function calcConfidence(nodes: Array<{ width: number; height: number; childCount: number }>): number {
11
11
  if (nodes.length < 2) return 100
12
- const base = nodes[0]!
12
+ const base = nodes[0]
13
13
  let score = 0
14
14
  for (const node of nodes.slice(1)) {
15
15
  const sizeDiff = Math.abs(node.width - base.width) + Math.abs(node.height - base.height)
@@ -24,7 +24,7 @@ function calcConfidence(nodes: Array<{ width: number; height: number; childCount
24
24
 
25
25
  function formatSignature(sig: string): string {
26
26
  const [typeSize, children] = sig.split('|')
27
- const type = typeSize?.split(':')[0]
27
+ const type = typeSize.split(':')[0]
28
28
  if (!type) return sig
29
29
  const typeName = type.charAt(0) + type.slice(1).toLowerCase()
30
30
  if (!children) return typeName
@@ -73,7 +73,7 @@ export default defineCommand({
73
73
  console.log('')
74
74
 
75
75
  const items = data.clusters.map((c) => {
76
- const first = c.nodes[0]!
76
+ const first = c.nodes[0]
77
77
  const confidence = calcConfidence(c.nodes)
78
78
 
79
79
  const widths = c.nodes.map((n) => n.width)
@@ -102,10 +102,11 @@ export default defineCommand({
102
102
 
103
103
  const clusteredNodes = data.clusters.reduce((sum, c) => sum + c.nodes.length, 0)
104
104
  console.log('')
105
- console.log(
106
- fmtSummary({ clusters: data.clusters.length }) +
107
- ` from ${data.totalNodes} nodes (${clusteredNodes} clustered)`
108
- )
105
+ console.log(fmtSummary({
106
+ clusters: data.clusters.length,
107
+ 'total nodes': data.totalNodes,
108
+ clustered: clusteredNodes
109
+ }))
109
110
  console.log('')
110
111
  }
111
112
  })
@@ -91,7 +91,7 @@ export default defineCommand({
91
91
  }
92
92
 
93
93
  console.log('')
94
- console.log(fmtSummary({ 'unique styles': data.styles.length }) + ` from ${data.totalTextNodes} text nodes`)
94
+ console.log(fmtSummary({ 'unique styles': data.styles.length, 'text nodes': data.totalTextNodes }))
95
95
  console.log('')
96
96
  }
97
97
  })
@@ -6,9 +6,17 @@ import { loadDocument } from '../headless'
6
6
  import { isAppMode, requireFile, rpc } from '../app-client'
7
7
  import { printError } from '../format'
8
8
 
9
+ function printResult(value: unknown, json: boolean) {
10
+ if (json || !process.stdout.isTTY) {
11
+ console.log(JSON.stringify(value, null, 2))
12
+ } else {
13
+ console.log(value)
14
+ }
15
+ }
16
+
9
17
  function serializeResult(value: unknown): unknown {
10
18
  if (value === undefined || value === null) return value
11
- if (typeof value === 'object' && value !== null && 'toJSON' in value && typeof value.toJSON === 'function') {
19
+ if (typeof value === 'object' && 'toJSON' in value && typeof value.toJSON === 'function') {
12
20
  return value.toJSON()
13
21
  }
14
22
  if (Array.isArray(value)) return value.map(serializeResult)
@@ -22,7 +30,7 @@ export default defineCommand({
22
30
  code: { type: 'string', alias: 'c', description: 'JavaScript code to execute' },
23
31
  stdin: { type: 'boolean', description: 'Read code from stdin' },
24
32
  write: { type: 'boolean', alias: 'w', description: 'Write changes back to the input file' },
25
- output: { type: 'string', alias: 'o', description: 'Write to a different file' },
33
+ output: { type: 'string', alias: 'o', description: 'Write to a different file', required: false },
26
34
  json: { type: 'boolean', description: 'Output as JSON' },
27
35
  quiet: { type: 'boolean', alias: 'q', description: 'Suppress output' },
28
36
  },
@@ -41,13 +49,9 @@ export default defineCommand({
41
49
  }
42
50
 
43
51
  if (isAppMode(args.file)) {
44
- const result = await rpc<unknown>('eval', { code })
52
+ const result = await rpc('eval', { code })
45
53
  if (!args.quiet && result !== undefined && result !== null) {
46
- if (args.json || !process.stdout.isTTY) {
47
- console.log(JSON.stringify(result, null, 2))
48
- } else {
49
- console.log(result)
50
- }
54
+ printResult(result, !!args.json)
51
55
  }
52
56
  return
53
57
  }
@@ -56,6 +60,7 @@ export default defineCommand({
56
60
  const graph = await loadDocument(file)
57
61
  const figma = new FigmaAPI(graph)
58
62
 
63
+ // eslint-disable-next-line no-empty-function -- needed to get AsyncFunction constructor
59
64
  const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
60
65
  const wrappedCode = code.trim().startsWith('return')
61
66
  ? code
@@ -71,17 +76,12 @@ export default defineCommand({
71
76
  }
72
77
 
73
78
  if (!args.quiet && result !== undefined) {
74
- const serialized = serializeResult(result)
75
- if (args.json || !process.stdout.isTTY) {
76
- console.log(JSON.stringify(serialized, null, 2))
77
- } else {
78
- console.log(serialized)
79
- }
79
+ printResult(serializeResult(result), !!args.json)
80
80
  }
81
81
 
82
82
  if (args.write || args.output) {
83
83
  const { exportFigFile } = await import('@open-pencil/core')
84
- const outPath = args.output ?? file
84
+ const outPath = args.output ? args.output : file
85
85
  const data = await exportFigFile(graph)
86
86
  await Bun.write(outPath, new Uint8Array(data))
87
87
  if (!args.quiet) {
@@ -12,16 +12,109 @@ const RASTER_FORMATS = ['PNG', 'JPG', 'WEBP']
12
12
  const ALL_FORMATS = [...RASTER_FORMATS, 'SVG', 'JSX']
13
13
  const JSX_STYLES = ['openpencil', 'tailwind']
14
14
 
15
+ interface ExportArgs {
16
+ file?: string
17
+ output?: string
18
+ format: string
19
+ scale: string
20
+ quality?: string
21
+ page?: string
22
+ node?: string
23
+ style: string
24
+ thumbnail?: boolean
25
+ width: string
26
+ height: string
27
+ }
28
+
29
+ async function writeAndLog(path: string, content: string | Uint8Array) {
30
+ await Bun.write(path, content)
31
+ const size = typeof content === 'string' ? content.length : content.length
32
+ console.log(ok(`Exported ${path} (${(size / 1024).toFixed(1)} KB)`))
33
+ }
34
+
35
+ async function exportViaApp(format: string, args: ExportArgs) {
36
+ if (format === 'SVG') {
37
+ const result = await rpc<{ svg: string }>('tool', { name: 'export_svg', args: { ids: args.node ? [args.node] : undefined } })
38
+ if (!result.svg) { printError('Nothing to export.'); process.exit(1) }
39
+ await writeAndLog(resolve(args.output ?? 'export.svg'), result.svg)
40
+ return
41
+ }
42
+
43
+ if (format === 'JSX') {
44
+ const result = await rpc<{ jsx: string }>('export_jsx', { nodeIds: args.node ? [args.node] : undefined, style: args.style })
45
+ if (!result.jsx) { printError('Nothing to export.'); process.exit(1) }
46
+ await writeAndLog(resolve(args.output ?? 'export.jsx'), result.jsx)
47
+ return
48
+ }
49
+
50
+ const result = await rpc<{ base64: string }>('export', {
51
+ nodeIds: args.node ? [args.node] : undefined,
52
+ scale: Number(args.scale),
53
+ format: format.toLowerCase()
54
+ })
55
+ const data = Uint8Array.from(atob(result.base64), (c) => c.charCodeAt(0))
56
+ const ext = format.toLowerCase() === 'jpg' ? 'jpg' : format.toLowerCase()
57
+ await writeAndLog(resolve(args.output ?? `export.${ext}`), data)
58
+ }
59
+
60
+ async function exportFromFile(format: string, args: ExportArgs) {
61
+ const file = requireFile(args.file)
62
+ const graph = await loadDocument(file)
63
+ await loadFonts(graph)
64
+
65
+ const pages = graph.getPages()
66
+ const page = args.page ? pages.find((p) => p.name === args.page) : pages[0]
67
+ if (!page) { printError(`Page "${args.page}" not found.`); process.exit(1) }
68
+
69
+ const defaultName = basename(file, extname(file))
70
+
71
+ if (format === 'JSX') {
72
+ const nodeIds = args.node ? [args.node] : page.childIds
73
+ const jsxStr = nodeIds.length === 1
74
+ ? sceneNodeToJSX(nodeIds[0], graph, args.style as JSXFormat)
75
+ : selectionToJSX(nodeIds, graph, args.style as JSXFormat)
76
+ if (!jsxStr) { printError('Nothing to export (empty page or no visible nodes).'); process.exit(1) }
77
+ await writeAndLog(resolve(args.output ?? `${defaultName}.jsx`), jsxStr)
78
+ return
79
+ }
80
+
81
+ const ext = format.toLowerCase() === 'jpg' ? 'jpg' : format.toLowerCase()
82
+ const output = resolve(args.output ?? `${defaultName}.${ext}`)
83
+
84
+ if (format === 'SVG') {
85
+ const nodeIds = args.node ? [args.node] : page.childIds
86
+ const svgStr = renderNodesToSVG(graph, page.id, nodeIds)
87
+ if (!svgStr) { printError('Nothing to export (empty page or no visible nodes).'); process.exit(1) }
88
+ await writeAndLog(output, svgStr)
89
+ return
90
+ }
91
+
92
+ let data: Uint8Array | null
93
+ if (args.thumbnail) {
94
+ data = await exportThumbnail(graph, page.id, Number(args.width), Number(args.height))
95
+ } else {
96
+ const nodeIds = args.node ? [args.node] : page.childIds
97
+ data = await exportNodes(graph, page.id, nodeIds, {
98
+ scale: Number(args.scale),
99
+ format: format as ExportFormat,
100
+ quality: args.quality ? Number(args.quality) : undefined
101
+ })
102
+ }
103
+
104
+ if (!data) { printError('Nothing to export (empty page or no visible nodes).'); process.exit(1) }
105
+ await writeAndLog(output, data)
106
+ }
107
+
15
108
  export default defineCommand({
16
109
  meta: { description: 'Export a .fig file to PNG, JPG, WEBP, SVG, or JSX' },
17
110
  args: {
18
111
  file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
19
- output: { type: 'string', alias: 'o', description: 'Output file path (default: <name>.<format>)' },
112
+ output: { type: 'string', alias: 'o', description: 'Output file path (default: <name>.<format>)', required: false },
20
113
  format: { type: 'string', alias: 'f', description: 'Export format: png, jpg, webp, svg, jsx (default: png)', default: 'png' },
21
114
  scale: { type: 'string', alias: 's', description: 'Export scale (default: 1)', default: '1' },
22
- quality: { type: 'string', alias: 'q', description: 'Quality 0-100 for JPG/WEBP (default: 90)' },
23
- page: { type: 'string', description: 'Page name (default: first page)' },
24
- node: { type: 'string', description: 'Node ID to export (default: all top-level nodes)' },
115
+ quality: { type: 'string', alias: 'q', description: 'Quality 0-100 for JPG/WEBP (default: 90)', required: false },
116
+ page: { type: 'string', description: 'Page name (default: first page)', required: false },
117
+ node: { type: 'string', description: 'Node ID to export (default: all top-level nodes)', required: false },
25
118
  style: { type: 'string', description: 'JSX style: openpencil, tailwind (default: openpencil)', default: 'openpencil' },
26
119
  thumbnail: { type: 'boolean', description: 'Export page thumbnail instead of full render' },
27
120
  width: { type: 'string', description: 'Thumbnail width (default: 1920)', default: '1920' },
@@ -40,114 +133,9 @@ export default defineCommand({
40
133
  }
41
134
 
42
135
  if (isAppMode(args.file)) {
43
- if (format === 'SVG') {
44
- const result = await rpc<{ svg: string }>('tool', { name: 'export_svg', args: { ids: args.node ? [args.node] : undefined } })
45
- if (!result.svg) {
46
- printError('Nothing to export.')
47
- process.exit(1)
48
- }
49
- const output = resolve(args.output ?? 'export.svg')
50
- await Bun.write(output, result.svg)
51
- console.log(ok(`Exported ${output} (${(result.svg.length / 1024).toFixed(1)} KB)`))
52
- return
53
- }
54
-
55
- if (format === 'JSX') {
56
- const result = await rpc<{ jsx: string }>('export_jsx', {
57
- nodeIds: args.node ? [args.node] : undefined,
58
- style: args.style
59
- })
60
- if (!result.jsx) {
61
- printError('Nothing to export.')
62
- process.exit(1)
63
- }
64
- const output = resolve(args.output ?? 'export.jsx')
65
- await Bun.write(output, result.jsx)
66
- console.log(ok(`Exported ${output} (${(result.jsx.length / 1024).toFixed(1)} KB)`))
67
- return
68
- }
69
-
70
- const result = await rpc<{ base64: string }>('export', {
71
- nodeIds: args.node ? [args.node] : undefined,
72
- scale: Number(args.scale),
73
- format: format.toLowerCase()
74
- })
75
- const data = Uint8Array.from(atob(result.base64), (c) => c.charCodeAt(0))
76
- const ext = format.toLowerCase() === 'jpg' ? 'jpg' : format.toLowerCase()
77
- const output = resolve(args.output ?? `export.${ext}`)
78
- await Bun.write(output, data)
79
- console.log(ok(`Exported ${output} (${(data.length / 1024).toFixed(1)} KB)`))
80
- return
81
- }
82
-
83
- const file = requireFile(args.file)
84
- const graph = await loadDocument(file)
85
- await loadFonts(graph)
86
-
87
- const pages = graph.getPages()
88
- const page = args.page
89
- ? pages.find((p) => p.name === args.page)
90
- : pages[0]
91
-
92
- if (!page) {
93
- printError(`Page "${args.page}" not found.`)
94
- process.exit(1)
95
- }
96
-
97
- const defaultName = basename(file, extname(file))
98
-
99
- if (format === 'JSX') {
100
- const jsxFormat = args.style as JSXFormat
101
- const nodeIds = args.node ? [args.node] : page.childIds
102
- const jsxStr = nodeIds.length === 1
103
- ? sceneNodeToJSX(nodeIds[0], graph, jsxFormat)
104
- : selectionToJSX(nodeIds, graph, jsxFormat)
105
-
106
- if (!jsxStr) {
107
- printError('Nothing to export (empty page or no visible nodes).')
108
- process.exit(1)
109
- }
110
-
111
- const output = resolve(args.output ?? `${defaultName}.jsx`)
112
- await Bun.write(output, jsxStr)
113
- console.log(ok(`Exported ${output} (${(jsxStr.length / 1024).toFixed(1)} KB)`))
114
- return
115
- }
116
-
117
- const ext = format.toLowerCase() === 'jpg' ? 'jpg' : format.toLowerCase()
118
- const output = resolve(args.output ?? `${defaultName}.${ext}`)
119
-
120
- if (format === 'SVG') {
121
- const nodeIds = args.node ? [args.node] : page.childIds
122
- const svgStr = renderNodesToSVG(graph, page.id, nodeIds)
123
- if (!svgStr) {
124
- printError('Nothing to export (empty page or no visible nodes).')
125
- process.exit(1)
126
- }
127
- await Bun.write(output, svgStr)
128
- console.log(ok(`Exported ${output} (${(svgStr.length / 1024).toFixed(1)} KB)`))
129
- return
130
- }
131
-
132
- let data: Uint8Array | null
133
-
134
- if (args.thumbnail) {
135
- data = await exportThumbnail(graph, page.id, Number(args.width), Number(args.height))
136
+ await exportViaApp(format, args)
136
137
  } else {
137
- const nodeIds = args.node ? [args.node] : page.childIds
138
- data = await exportNodes(graph, page.id, nodeIds, {
139
- scale: Number(args.scale),
140
- format,
141
- quality: args.quality ? Number(args.quality) : undefined
142
- })
138
+ await exportFromFile(format, args)
143
139
  }
144
-
145
- if (!data) {
146
- printError('Nothing to export (empty page or no visible nodes).')
147
- process.exit(1)
148
- }
149
-
150
- await Bun.write(output, data)
151
- console.log(ok(`Exported ${output} (${(data.length / 1024).toFixed(1)} KB)`))
152
140
  }
153
141
  })
@@ -2,7 +2,7 @@ import { defineCommand } from 'citty'
2
2
 
3
3
  import { loadDocument } from '../headless'
4
4
  import { isAppMode, requireFile, rpc } from '../app-client'
5
- import { fmtList, printError, bold, entity, formatType } from '../format'
5
+ import { fmtList, bold, entity, formatType } from '../format'
6
6
  import { executeRpcCommand } from '@open-pencil/core'
7
7
 
8
8
  import type { FindNodeResult } from '@open-pencil/core'
@@ -5,7 +5,7 @@ import { isAppMode, requireFile, rpc } from '../app-client'
5
5
  import { fmtNode, printError, formatType } from '../format'
6
6
  import { executeRpcCommand, colorToHex } from '@open-pencil/core'
7
7
 
8
- import type { NodeResult } from '@open-pencil/core'
8
+ import type { Color, NodeResult } from '@open-pencil/core'
9
9
 
10
10
  async function getData(file: string | undefined, id: string): Promise<NodeResult | { error: string }> {
11
11
  if (isAppMode(file)) return rpc<NodeResult>('node', { id })
@@ -47,7 +47,7 @@ export default defineCommand({
47
47
  if (data.parent) details.parent = `${data.parent.name} (${data.parent.id})`
48
48
  if (data.text) details.text = data.text
49
49
  if (data.fills.length > 0) {
50
- const solid = (data.fills as Array<{ type: string; visible: boolean; color: { r: number; g: number; b: number; a: number }; opacity: number }>)
50
+ const solid = (data.fills as Array<{ type: string; visible: boolean; color: Color; opacity: number }>)
51
51
  .find((f) => f.type === 'SOLID' && f.visible)
52
52
  if (solid) {
53
53
  const hex = colorToHex(solid.color)
@@ -2,7 +2,7 @@ import { defineCommand } from 'citty'
2
2
 
3
3
  import { loadDocument } from '../headless'
4
4
  import { isAppMode, requireFile, rpc } from '../app-client'
5
- import { bold, fmtList, entity, formatType } from '../format'
5
+ import { bold, fmtList, entity } from '../format'
6
6
 
7
7
  import type { PageItem } from '@open-pencil/core'
8
8
  import { executeRpcCommand } from '@open-pencil/core'
@@ -0,0 +1,90 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { loadDocument } from '../headless'
4
+ import { isAppMode, requireFile, rpc } from '../app-client'
5
+ import { fmtList, printError, bold, entity, formatType } from '../format'
6
+ import { executeRpcCommand } from '@open-pencil/core'
7
+
8
+ import type { QueryNodeResult } from '@open-pencil/core'
9
+
10
+ async function getData(
11
+ file: string | undefined,
12
+ args: { selector: string; page?: string; limit?: string }
13
+ ): Promise<QueryNodeResult[] | { error: string }> {
14
+ const rpcArgs = {
15
+ selector: args.selector,
16
+ page: args.page,
17
+ limit: args.limit ? Number(args.limit) : undefined
18
+ }
19
+ if (isAppMode(file)) return rpc<QueryNodeResult[]>('query', rpcArgs)
20
+ const graph = await loadDocument(requireFile(file))
21
+ return await executeRpcCommand(graph, 'query', rpcArgs) as QueryNodeResult[] | { error: string }
22
+ }
23
+
24
+ export default defineCommand({
25
+ meta: {
26
+ description: `Query nodes using XPath selectors
27
+
28
+ Examples:
29
+ open-pencil query file.fig "//FRAME" # All frames
30
+ open-pencil query file.fig "//FRAME[@width < 300]" # Frames narrower than 300px
31
+ open-pencil query file.fig "//COMPONENT[starts-with(@name, 'Button')]" # Components starting with Button
32
+ open-pencil query file.fig "//SECTION/FRAME" # Direct frame children of sections
33
+ open-pencil query file.fig "//SECTION//TEXT" # All text inside sections
34
+ open-pencil query file.fig "//*[@cornerRadius > 0]" # Any node with corner radius`
35
+ },
36
+ args: {
37
+ file: {
38
+ type: 'positional',
39
+ description: '.fig file path (omit to connect to running app)',
40
+ required: false
41
+ },
42
+ selector: {
43
+ type: 'positional',
44
+ description:
45
+ 'XPath selector (e.g., //FRAME[@width < 300], //TEXT[contains(@name, "Label")])',
46
+ required: true
47
+ },
48
+ page: { type: 'string', description: 'Page name (default: all pages)' },
49
+ limit: { type: 'string', description: 'Max results (default: 1000)', default: '1000' },
50
+ json: { type: 'boolean', description: 'Output as JSON' }
51
+ },
52
+ async run({ args }) {
53
+ const results = await getData(args.file, {
54
+ selector: args.selector,
55
+ page: args.page,
56
+ limit: args.limit
57
+ })
58
+
59
+ if ('error' in results) {
60
+ printError(results.error)
61
+ process.exit(1)
62
+ }
63
+
64
+ if (args.json) {
65
+ console.log(JSON.stringify(results, null, 2))
66
+ return
67
+ }
68
+
69
+ if (results.length === 0) {
70
+ console.log('No nodes found.')
71
+ return
72
+ }
73
+
74
+ console.log('')
75
+ console.log(bold(` Found ${results.length} node${results.length > 1 ? 's' : ''}`))
76
+ console.log('')
77
+ console.log(
78
+ fmtList(
79
+ results.map((n) => ({
80
+ header: entity(
81
+ formatType(n.type),
82
+ `${n.name} ${n.width}×${n.height}`,
83
+ n.id
84
+ )
85
+ }))
86
+ )
87
+ )
88
+ console.log('')
89
+ }
90
+ })
@@ -2,7 +2,7 @@ import { defineCommand } from 'citty'
2
2
 
3
3
  import { loadDocument } from '../headless'
4
4
  import { isAppMode, requireFile, rpc } from '../app-client'
5
- import { bold, fmtList, fmtSummary } from '../format'
5
+ import { bold, entity, fmtList, fmtSummary } from '../format'
6
6
  import { executeRpcCommand } from '@open-pencil/core'
7
7
 
8
8
  import type { VariablesResult } from '@open-pencil/core'
@@ -38,7 +38,7 @@ export default defineCommand({
38
38
  console.log('')
39
39
 
40
40
  for (const coll of data.collections) {
41
- console.log(bold(` ${coll.name}`) + ` (${coll.modes.join(', ')})`)
41
+ console.log(bold(entity(coll.name, coll.modes.join(', '))))
42
42
  console.log('')
43
43
  console.log(
44
44
  fmtList(
package/src/format.ts CHANGED
@@ -48,7 +48,7 @@ export function formatBox(node: SceneNode): string {
48
48
  function formatFill(node: SceneNode): string | null {
49
49
  if (!node.fills.length) return null
50
50
  const solid = node.fills.find((f) => f.type === 'SOLID' && f.visible)
51
- if (!solid || solid.type !== 'SOLID') return null
51
+ if (!solid?.color) return null
52
52
  const { r, g, b } = solid.color
53
53
  const hex = '#' + [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('')
54
54
  return solid.opacity < 1 ? `${hex} ${Math.round(solid.opacity * 100)}%` : hex
@@ -56,7 +56,7 @@ function formatFill(node: SceneNode): string | null {
56
56
 
57
57
  function formatStroke(node: SceneNode): string | null {
58
58
  if (!node.strokes.length) return null
59
- const s = node.strokes[0]!
59
+ const s = node.strokes[0]
60
60
  const { r, g, b } = s.color
61
61
  const hex = '#' + [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('')
62
62
  return `${hex} ${s.weight}px`
package/src/headless.ts CHANGED
@@ -2,14 +2,14 @@ import CanvasKitInit from 'canvaskit-wasm/full'
2
2
  import type { CanvasKit } from 'canvaskit-wasm'
3
3
  import {
4
4
  parseFigFile,
5
- SceneGraph,
5
+ type SceneGraph,
6
+ type ExportFormat,
6
7
  SkiaRenderer,
7
8
  computeAllLayouts,
8
9
  loadFont,
9
10
  renderNodesToImage,
10
11
  renderThumbnail
11
12
  } from '@open-pencil/core'
12
- import type { ExportFormat } from '@open-pencil/core'
13
13
 
14
14
  let ck: CanvasKit | null = null
15
15
 
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import evalCmd from './commands/eval'
6
6
  import exportCmd from './commands/export'
7
7
  import find from './commands/find'
8
8
  import info from './commands/info'
9
+ import query from './commands/query'
9
10
  import node from './commands/node'
10
11
  import pages from './commands/pages'
11
12
  import tree from './commands/tree'
@@ -25,6 +26,7 @@ const main = defineCommand({
25
26
  export: exportCmd,
26
27
  find,
27
28
  info,
29
+ query,
28
30
  node,
29
31
  pages,
30
32
  tree,
@@ -32,4 +34,4 @@ const main = defineCommand({
32
34
  }
33
35
  })
34
36
 
35
- runMain(main)
37
+ void runMain(main)
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Danila Poyarkov
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.