@open-pencil/cli 0.10.0 → 0.11.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.
@@ -1,14 +1,23 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
4
- import { isAppMode, requireFile, rpc } from '../app-client'
5
- import { fmtList, bold, entity, formatType } from '../format'
6
3
  import { executeRpcCommand } from '@open-pencil/core'
7
4
 
5
+ import { isAppMode, requireFile, rpc } from '../app-client'
6
+ import { printNodeResults } from '../format'
7
+ import { loadDocument } from '../headless'
8
+
8
9
  import type { FindNodeResult } from '@open-pencil/core'
9
10
 
10
- async function getData(file: string | undefined, args: { name?: string; type?: string; page?: string; limit?: string }): Promise<FindNodeResult[]> {
11
- const rpcArgs = { name: args.name, type: args.type, page: args.page, limit: args.limit ? Number(args.limit) : undefined }
11
+ async function getData(
12
+ file: string | undefined,
13
+ args: { name?: string; type?: string; page?: string; limit?: string }
14
+ ): Promise<FindNodeResult[]> {
15
+ const rpcArgs = {
16
+ name: args.name,
17
+ type: args.type,
18
+ page: args.page,
19
+ limit: args.limit ? Number(args.limit) : undefined
20
+ }
12
21
  if (isAppMode(file)) return rpc<FindNodeResult[]>('find', rpcArgs)
13
22
  const graph = await loadDocument(requireFile(file))
14
23
  return executeRpcCommand(graph, 'find', rpcArgs) as FindNodeResult[]
@@ -17,7 +26,11 @@ async function getData(file: string | undefined, args: { name?: string; type?: s
17
26
  export default defineCommand({
18
27
  meta: { description: 'Find nodes by name or type' },
19
28
  args: {
20
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
29
+ file: {
30
+ type: 'positional',
31
+ description: 'Document file path (omit to connect to running app)',
32
+ required: false
33
+ },
21
34
  name: { type: 'string', description: 'Node name (partial match, case-insensitive)' },
22
35
  type: { type: 'string', description: 'Node type: FRAME, TEXT, RECTANGLE, INSTANCE, etc.' },
23
36
  page: { type: 'string', description: 'Page name (default: all pages)' },
@@ -32,21 +45,6 @@ export default defineCommand({
32
45
  return
33
46
  }
34
47
 
35
- if (results.length === 0) {
36
- console.log('No nodes found.')
37
- return
38
- }
39
-
40
- console.log('')
41
- console.log(bold(` Found ${results.length} node${results.length > 1 ? 's' : ''}`))
42
- console.log('')
43
- console.log(
44
- fmtList(
45
- results.map((n) => ({
46
- header: entity(formatType(n.type), n.name, n.id)
47
- }))
48
- )
49
- )
50
- console.log('')
48
+ printNodeResults(results)
51
49
  }
52
50
  })
@@ -0,0 +1,80 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core'
4
+
5
+ import { bold, fmtList, kv } from '../format'
6
+
7
+ const io = new IORegistry(BUILTIN_IO_FORMATS)
8
+
9
+ function supportLabels(format: ReturnType<IORegistry['listFormats']>[number]): string[] {
10
+ const labels: string[] = []
11
+ if (format.support.readDocument) labels.push('read')
12
+ if (format.support.writeDocument) labels.push('write')
13
+ if (format.support.exportDocument) labels.push('export-document')
14
+ if (format.support.exportPage) labels.push('export-page')
15
+ if (format.support.exportSelection) labels.push('export-selection')
16
+ if (format.support.exportNode) labels.push('export-node')
17
+ return labels
18
+ }
19
+
20
+ export default defineCommand({
21
+ meta: { description: 'List supported document and export formats' },
22
+ args: {
23
+ json: { type: 'boolean', description: 'Output as JSON' }
24
+ },
25
+ async run({ args }) {
26
+ const formats = io.listFormats().map((format) => ({
27
+ id: format.id,
28
+ label: format.label,
29
+ role: format.role,
30
+ category: format.category,
31
+ extensions: format.extensions,
32
+ mimeTypes: format.mimeTypes,
33
+ support: supportLabels(format)
34
+ }))
35
+
36
+ if (args.json) {
37
+ console.log(JSON.stringify(formats, null, 2))
38
+ return
39
+ }
40
+
41
+ console.log('')
42
+ console.log(bold(` ${formats.length} format${formats.length !== 1 ? 's' : ''}`))
43
+ console.log('')
44
+ console.log(
45
+ fmtList(
46
+ formats.map((format) => ({
47
+ header: `${format.label} (${format.id})`,
48
+ details: {
49
+ role: format.role,
50
+ category: format.category,
51
+ ext: format.extensions.map((ext) => `.${ext}`).join(', '),
52
+ support: format.support.join(', '),
53
+ mime: format.mimeTypes.join(', ')
54
+ }
55
+ })),
56
+ { compact: true }
57
+ )
58
+ )
59
+ console.log('')
60
+ console.log(
61
+ kv(
62
+ 'Readable',
63
+ io
64
+ .listReadableFormats()
65
+ .map((f) => f.id)
66
+ .join(', ') || 'none'
67
+ )
68
+ )
69
+ console.log(
70
+ kv(
71
+ 'Writable',
72
+ io
73
+ .listWritableFormats()
74
+ .map((f) => f.id)
75
+ .join(', ') || 'none'
76
+ )
77
+ )
78
+ console.log('')
79
+ }
80
+ })
@@ -1,11 +1,12 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
3
+ import { executeRpcCommand } from '@open-pencil/core'
4
+
4
5
  import { isAppMode, requireFile, rpc } from '../app-client'
5
6
  import { bold, fmtHistogram, fmtSummary, kv } from '../format'
7
+ import { loadDocument } from '../headless'
6
8
 
7
9
  import type { InfoResult } from '@open-pencil/core'
8
- import { executeRpcCommand } from '@open-pencil/core'
9
10
 
10
11
  async function getData(file?: string): Promise<InfoResult> {
11
12
  if (isAppMode(file)) return rpc<InfoResult>('info')
@@ -16,7 +17,11 @@ async function getData(file?: string): Promise<InfoResult> {
16
17
  export default defineCommand({
17
18
  meta: { description: 'Show document info (pages, node counts, fonts)' },
18
19
  args: {
19
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
20
+ file: {
21
+ type: 'positional',
22
+ description: 'Document file path (omit to connect to running app)',
23
+ required: false
24
+ },
20
25
  json: { type: 'boolean', description: 'Output as JSON' }
21
26
  },
22
27
  async run({ args }) {
@@ -0,0 +1,86 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import { allRules, createLinter, presets, type LintMessage } from '@open-pencil/core'
4
+
5
+ import { bold, dim, fail, fmtList, ok } from '../format'
6
+ import { loadDocument } from '../headless'
7
+
8
+ function formatSeverity(severity: LintMessage['severity']) {
9
+ if (severity === 'error') return fail('error')
10
+ if (severity === 'warning') return fail('warn')
11
+ return ok('info')
12
+ }
13
+
14
+ function formatMessage(message: LintMessage) {
15
+ return {
16
+ header: `${formatSeverity(message.severity)} ${bold(message.ruleId)} ${dim(message.nodePath.join(' / '))}`,
17
+ details: {
18
+ message: message.message,
19
+ node: `${message.nodeName} (${message.nodeId})`,
20
+ suggest: message.suggest
21
+ }
22
+ }
23
+ }
24
+
25
+ export default defineCommand({
26
+ meta: {
27
+ name: 'lint',
28
+ description: 'Lint design documents for consistency, structure, and accessibility issues'
29
+ },
30
+ args: {
31
+ file: {
32
+ type: 'positional',
33
+ required: true,
34
+ description: 'Design document to lint (.fig, .pen)'
35
+ },
36
+ preset: {
37
+ type: 'string',
38
+ default: 'recommended',
39
+ description: 'Preset: recommended, strict, accessibility'
40
+ },
41
+ rule: { type: 'string', description: 'Run specific rule(s) only (repeatable)' },
42
+ json: { type: 'boolean', default: false, description: 'Output as JSON' },
43
+ 'list-rules': { type: 'boolean', default: false, description: 'List rules and exit' }
44
+ },
45
+ async run({ args }) {
46
+ if (args['list-rules']) {
47
+ console.log('')
48
+ console.log(bold('Available rules'))
49
+ console.log('')
50
+ console.log(
51
+ fmtList(
52
+ Object.entries(allRules).map(([id, rule]) => ({
53
+ header: bold(id),
54
+ details: { category: rule.meta.category, description: rule.meta.description }
55
+ }))
56
+ )
57
+ )
58
+ console.log('')
59
+ console.log(bold(`Presets: ${Object.keys(presets).join(', ')}`))
60
+ console.log('')
61
+ return
62
+ }
63
+
64
+ const graph = await loadDocument(args.file)
65
+ const rules = args.rule ? (Array.isArray(args.rule) ? args.rule : [args.rule]) : undefined
66
+ const result = createLinter({ preset: args.preset, rules }).lintGraph(graph)
67
+
68
+ if (args.json) {
69
+ console.log(JSON.stringify(result, null, 2))
70
+ } else if (result.messages.length === 0) {
71
+ console.log(ok('No lint issues found.'))
72
+ } else {
73
+ console.log('')
74
+ console.log(
75
+ bold(
76
+ `Lint issues: ${result.errorCount} errors, ${result.warningCount} warnings, ${result.infoCount} info`
77
+ )
78
+ )
79
+ console.log('')
80
+ console.log(fmtList(result.messages.map(formatMessage)))
81
+ console.log('')
82
+ }
83
+
84
+ if (result.errorCount > 0) process.exit(1)
85
+ }
86
+ })
@@ -1,13 +1,17 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
3
+ import { executeRpcCommand, colorToHex } from '@open-pencil/core'
4
+
4
5
  import { isAppMode, requireFile, rpc } from '../app-client'
5
6
  import { fmtNode, printError, formatType } from '../format'
6
- import { executeRpcCommand, colorToHex } from '@open-pencil/core'
7
+ import { loadDocument } from '../headless'
7
8
 
8
9
  import type { Color, NodeResult } from '@open-pencil/core'
9
10
 
10
- async function getData(file: string | undefined, id: string): Promise<NodeResult | { error: string }> {
11
+ async function getData(
12
+ file: string | undefined,
13
+ id: string
14
+ ): Promise<NodeResult | { error: string }> {
11
15
  if (isAppMode(file)) return rpc<NodeResult>('node', { id })
12
16
  const graph = await loadDocument(requireFile(file))
13
17
  return executeRpcCommand(graph, 'node', { id }) as NodeResult | { error: string }
@@ -16,7 +20,11 @@ async function getData(file: string | undefined, id: string): Promise<NodeResult
16
20
  export default defineCommand({
17
21
  meta: { description: 'Show detailed node properties by ID' },
18
22
  args: {
19
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
23
+ file: {
24
+ type: 'positional',
25
+ description: 'Document file path (omit to connect to running app)',
26
+ required: false
27
+ },
20
28
  id: { type: 'string', description: 'Node ID', required: true },
21
29
  json: { type: 'boolean', description: 'Output as JSON' }
22
30
  },
@@ -47,8 +55,9 @@ export default defineCommand({
47
55
  if (data.parent) details.parent = `${data.parent.name} (${data.parent.id})`
48
56
  if (data.text) details.text = data.text
49
57
  if (data.fills.length > 0) {
50
- const solid = (data.fills as Array<{ type: string; visible: boolean; color: Color; opacity: number }>)
51
- .find((f) => f.type === 'SOLID' && f.visible)
58
+ const solid = (
59
+ data.fills as Array<{ type: string; visible: boolean; color: Color; opacity: number }>
60
+ ).find((f) => f.type === 'SOLID' && f.visible)
52
61
  if (solid) {
53
62
  const hex = colorToHex(solid.color)
54
63
  details.fill = solid.opacity < 1 ? `${hex} ${Math.round(solid.opacity * 100)}%` : hex
@@ -1,11 +1,12 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
3
+ import { executeRpcCommand } from '@open-pencil/core'
4
+
4
5
  import { isAppMode, requireFile, rpc } from '../app-client'
5
6
  import { bold, fmtList, entity } from '../format'
7
+ import { loadDocument } from '../headless'
6
8
 
7
9
  import type { PageItem } from '@open-pencil/core'
8
- import { executeRpcCommand } from '@open-pencil/core'
9
10
 
10
11
  async function getData(file?: string): Promise<PageItem[]> {
11
12
  if (isAppMode(file)) return rpc<PageItem[]>('pages')
@@ -14,9 +15,13 @@ async function getData(file?: string): Promise<PageItem[]> {
14
15
  }
15
16
 
16
17
  export default defineCommand({
17
- meta: { description: 'List pages in a .fig file' },
18
+ meta: { description: 'List pages in a document' },
18
19
  args: {
19
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
20
+ file: {
21
+ type: 'positional',
22
+ description: 'Document file path (omit to connect to running app)',
23
+ required: false
24
+ },
20
25
  json: { type: 'boolean', description: 'Output as JSON' }
21
26
  },
22
27
  async run({ args }) {
@@ -1,10 +1,11 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
4
- import { isAppMode, requireFile, rpc } from '../app-client'
5
- import { fmtList, printError, bold, entity, formatType } from '../format'
6
3
  import { executeRpcCommand } from '@open-pencil/core'
7
4
 
5
+ import { isAppMode, requireFile, rpc } from '../app-client'
6
+ import { printNodeResults, printError } from '../format'
7
+ import { loadDocument } from '../headless'
8
+
8
9
  import type { QueryNodeResult } from '@open-pencil/core'
9
10
 
10
11
  async function getData(
@@ -18,7 +19,7 @@ async function getData(
18
19
  }
19
20
  if (isAppMode(file)) return rpc<QueryNodeResult[]>('query', rpcArgs)
20
21
  const graph = await loadDocument(requireFile(file))
21
- return await executeRpcCommand(graph, 'query', rpcArgs) as QueryNodeResult[] | { error: string }
22
+ return (await executeRpcCommand(graph, 'query', rpcArgs)) as QueryNodeResult[] | { error: string }
22
23
  }
23
24
 
24
25
  export default defineCommand({
@@ -30,19 +31,18 @@ Examples:
30
31
  open-pencil query file.fig "//FRAME[@width < 300]" # Frames narrower than 300px
31
32
  open-pencil query file.fig "//COMPONENT[starts-with(@name, 'Button')]" # Components starting with Button
32
33
  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 "//SECTION//TEXT" # All text inside sections
34
35
  open-pencil query file.fig "//*[@cornerRadius > 0]" # Any node with corner radius`
35
36
  },
36
37
  args: {
37
38
  file: {
38
39
  type: 'positional',
39
- description: '.fig file path (omit to connect to running app)',
40
+ description: 'Document file path (omit to connect to running app)',
40
41
  required: false
41
42
  },
42
43
  selector: {
43
44
  type: 'positional',
44
- description:
45
- 'XPath selector (e.g., //FRAME[@width < 300], //TEXT[contains(@name, "Label")])',
45
+ description: 'XPath selector (e.g., //FRAME[@width < 300], //TEXT[contains(@name, "Label")])',
46
46
  required: true
47
47
  },
48
48
  page: { type: 'string', description: 'Page name (default: all pages)' },
@@ -66,25 +66,9 @@ Examples:
66
66
  return
67
67
  }
68
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('')
69
+ printNodeResults(results, (n) => {
70
+ const q = n as { width?: number; height?: number; name: string }
71
+ return `${q.name} ${q.width}×${q.height}`
72
+ })
89
73
  }
90
74
  })
@@ -1,9 +1,10 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
3
+ import { executeRpcCommand } from '@open-pencil/core'
4
+
4
5
  import { isAppMode, requireFile, rpc } from '../app-client'
5
6
  import { fmtTree, printError, entity, formatType } from '../format'
6
- import { executeRpcCommand } from '@open-pencil/core'
7
+ import { loadDocument } from '../headless'
7
8
 
8
9
  import type { TreeResult, TreeNodeResult } from '@open-pencil/core'
9
10
  import type { TreeNode } from 'agentfmt'
@@ -18,7 +19,10 @@ function toAgentfmtTree(node: TreeNodeResult, maxDepth: number, depth = 0): Tree
18
19
  return treeNode
19
20
  }
20
21
 
21
- async function getData(file: string | undefined, args: { page?: string; depth?: string }): Promise<TreeResult | { error: string }> {
22
+ async function getData(
23
+ file: string | undefined,
24
+ args: { page?: string; depth?: string }
25
+ ): Promise<TreeResult | { error: string }> {
22
26
  const rpcArgs = { page: args.page, depth: args.depth ? Number(args.depth) : undefined }
23
27
  if (isAppMode(file)) return rpc<TreeResult>('tree', rpcArgs)
24
28
  const graph = await loadDocument(requireFile(file))
@@ -28,7 +32,11 @@ async function getData(file: string | undefined, args: { page?: string; depth?:
28
32
  export default defineCommand({
29
33
  meta: { description: 'Print the node tree' },
30
34
  args: {
31
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
35
+ file: {
36
+ type: 'positional',
37
+ description: 'Document file path (omit to connect to running app)',
38
+ required: false
39
+ },
32
40
  page: { type: 'string', description: 'Page name (default: first page)' },
33
41
  depth: { type: 'string', description: 'Max depth (default: unlimited)' },
34
42
  json: { type: 'boolean', description: 'Output as JSON' }
@@ -1,13 +1,17 @@
1
1
  import { defineCommand } from 'citty'
2
2
 
3
- import { loadDocument } from '../headless'
3
+ import { executeRpcCommand } from '@open-pencil/core'
4
+
4
5
  import { isAppMode, requireFile, rpc } from '../app-client'
5
6
  import { bold, entity, fmtList, fmtSummary } from '../format'
6
- import { executeRpcCommand } from '@open-pencil/core'
7
+ import { loadDocument } from '../headless'
7
8
 
8
9
  import type { VariablesResult } from '@open-pencil/core'
9
10
 
10
- async function getData(file: string | undefined, args: { collection?: string; type?: string }): Promise<VariablesResult> {
11
+ async function getData(
12
+ file: string | undefined,
13
+ args: { collection?: string; type?: string }
14
+ ): Promise<VariablesResult> {
11
15
  const rpcArgs = { collection: args.collection, type: args.type }
12
16
  if (isAppMode(file)) return rpc<VariablesResult>('variables', rpcArgs)
13
17
  const graph = await loadDocument(requireFile(file))
@@ -17,7 +21,11 @@ async function getData(file: string | undefined, args: { collection?: string; ty
17
21
  export default defineCommand({
18
22
  meta: { description: 'List design variables and collections' },
19
23
  args: {
20
- file: { type: 'positional', description: '.fig file path (omit to connect to running app)', required: false },
24
+ file: {
25
+ type: 'positional',
26
+ description: 'Document file path (omit to connect to running app)',
27
+ required: false
28
+ },
21
29
  collection: { type: 'string', description: 'Filter by collection name' },
22
30
  type: { type: 'string', description: 'Filter by type: COLOR, FLOAT, STRING, BOOLEAN' },
23
31
  json: { type: 'boolean', description: 'Output as JSON' }
package/src/format.ts CHANGED
@@ -13,10 +13,45 @@ import {
13
13
  histogram as fmtHistogram,
14
14
  summary as fmtSummary
15
15
  } from 'agentfmt'
16
- import type { TreeNode, ListItem, NodeData } from 'agentfmt'
16
+
17
17
  import type { SceneNode, SceneGraph } from '@open-pencil/core'
18
+ import type { TreeNode, ListItem, NodeData } from 'agentfmt'
19
+
20
+ export {
21
+ ok,
22
+ fail,
23
+ dim,
24
+ bold,
25
+ cyan,
26
+ entity,
27
+ kv,
28
+ fmtTree,
29
+ fmtList,
30
+ fmtNode,
31
+ fmtHistogram,
32
+ fmtSummary
33
+ }
18
34
 
19
- export { ok, fail, dim, bold, cyan, entity, kv, fmtTree, fmtList, fmtNode, fmtHistogram, fmtSummary }
35
+ export function printNodeResults(
36
+ results: Array<{ type: string; name: string; id: string }>,
37
+ formatLabel: (n: { type: string; name: string; id: string }) => string = (n) => n.name
38
+ ): void {
39
+ if (results.length === 0) {
40
+ console.log('No nodes found.')
41
+ return
42
+ }
43
+ console.log('')
44
+ console.log(bold(` Found ${results.length} node${results.length > 1 ? 's' : ''}`))
45
+ console.log('')
46
+ console.log(
47
+ fmtList(
48
+ results.map((n) => ({
49
+ header: entity(formatType(n.type), formatLabel(n), n.id)
50
+ }))
51
+ )
52
+ )
53
+ console.log('')
54
+ }
20
55
 
21
56
  const TYPE_LABELS: Record<string, string> = {
22
57
  FRAME: 'frame',
@@ -42,7 +77,12 @@ export function formatType(type: string): string {
42
77
  }
43
78
 
44
79
  export function formatBox(node: SceneNode): string {
45
- return fmtBox(Math.round(node.width), Math.round(node.height), Math.round(node.x), Math.round(node.y))
80
+ return fmtBox(
81
+ Math.round(node.width),
82
+ Math.round(node.height),
83
+ Math.round(node.x),
84
+ Math.round(node.y)
85
+ )
46
86
  }
47
87
 
48
88
  function formatFill(node: SceneNode): string | null {
@@ -50,7 +90,15 @@ function formatFill(node: SceneNode): string | null {
50
90
  const solid = node.fills.find((f) => f.type === 'SOLID' && f.visible)
51
91
  if (!solid?.color) return null
52
92
  const { r, g, b } = solid.color
53
- const hex = '#' + [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('')
93
+ const hex =
94
+ '#' +
95
+ [r, g, b]
96
+ .map((c) =>
97
+ Math.round(c * 255)
98
+ .toString(16)
99
+ .padStart(2, '0')
100
+ )
101
+ .join('')
54
102
  return solid.opacity < 1 ? `${hex} ${Math.round(solid.opacity * 100)}%` : hex
55
103
  }
56
104
 
@@ -58,7 +106,15 @@ function formatStroke(node: SceneNode): string | null {
58
106
  if (!node.strokes.length) return null
59
107
  const s = node.strokes[0]
60
108
  const { r, g, b } = s.color
61
- const hex = '#' + [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('')
109
+ const hex =
110
+ '#' +
111
+ [r, g, b]
112
+ .map((c) =>
113
+ Math.round(c * 255)
114
+ .toString(16)
115
+ .padStart(2, '0')
116
+ )
117
+ .join('')
62
118
  return `${hex} ${s.weight}px`
63
119
  }
64
120
 
@@ -122,7 +178,12 @@ export function nodeDetails(node: SceneNode): Record<string, unknown> {
122
178
  return details
123
179
  }
124
180
 
125
- export function nodeToTreeNode(graph: SceneGraph, node: SceneNode, maxDepth: number, depth = 0): TreeNode {
181
+ export function nodeToTreeNode(
182
+ graph: SceneGraph,
183
+ node: SceneNode,
184
+ maxDepth: number,
185
+ depth = 0
186
+ ): TreeNode {
126
187
  const treeNode: TreeNode = {
127
188
  header: entity(formatType(node.type), node.name, node.id),
128
189
  details: nodeDetails(node)
package/src/headless.ts CHANGED
@@ -1,36 +1,18 @@
1
1
  import {
2
- parseFigFile,
3
- initCanvasKit,
4
- type SceneGraph,
5
- type ExportFormat,
2
+ BUILTIN_IO_FORMATS,
3
+ IORegistry,
6
4
  computeAllLayouts,
7
- headlessRenderNodes,
8
- headlessRenderThumbnail
5
+ initCanvasKit,
6
+ type SceneGraph
9
7
  } from '@open-pencil/core'
10
8
 
11
9
  export { initCanvasKit }
12
10
 
11
+ const io = new IORegistry(BUILTIN_IO_FORMATS)
12
+
13
13
  export async function loadDocument(filePath: string): Promise<SceneGraph> {
14
- const data = await Bun.file(filePath).arrayBuffer()
15
- const graph = await parseFigFile(data)
14
+ const bytes = new Uint8Array(await Bun.file(filePath).arrayBuffer())
15
+ const { graph } = await io.readDocument({ name: filePath, data: bytes })
16
16
  computeAllLayouts(graph)
17
17
  return graph
18
18
  }
19
-
20
- export async function exportNodes(
21
- graph: SceneGraph,
22
- pageId: string,
23
- nodeIds: string[],
24
- options: { scale?: number; format?: ExportFormat; quality?: number }
25
- ): Promise<Uint8Array | null> {
26
- return headlessRenderNodes(graph, pageId, nodeIds, options)
27
- }
28
-
29
- export async function exportThumbnail(
30
- graph: SceneGraph,
31
- pageId: string,
32
- width: number,
33
- height: number
34
- ): Promise<Uint8Array | null> {
35
- return headlessRenderThumbnail(graph, pageId, width, height)
36
- }
package/src/index.ts CHANGED
@@ -2,13 +2,16 @@
2
2
  import { defineCommand, runMain } from 'citty'
3
3
 
4
4
  import analyze from './commands/analyze'
5
+ import convert from './commands/convert'
5
6
  import evalCmd from './commands/eval'
6
7
  import exportCmd from './commands/export'
7
8
  import find from './commands/find'
9
+ import formats from './commands/formats'
8
10
  import info from './commands/info'
9
- import query from './commands/query'
11
+ import lint from './commands/lint'
10
12
  import node from './commands/node'
11
13
  import pages from './commands/pages'
14
+ import query from './commands/query'
12
15
  import tree from './commands/tree'
13
16
  import variables from './commands/variables'
14
17
 
@@ -17,15 +20,18 @@ const { version } = await import('../package.json')
17
20
  const main = defineCommand({
18
21
  meta: {
19
22
  name: 'open-pencil',
20
- description: 'OpenPencil CLI — inspect, export, and lint .fig design files',
23
+ description: 'OpenPencil CLI — inspect, export, and lint OpenPencil design documents',
21
24
  version
22
25
  },
23
26
  subCommands: {
24
27
  analyze,
28
+ convert,
25
29
  eval: evalCmd,
26
30
  export: exportCmd,
27
31
  find,
32
+ formats,
28
33
  info,
34
+ lint,
29
35
  query,
30
36
  node,
31
37
  pages,