@open-pencil/cli 0.10.0 → 0.11.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 +2 -2
- package/src/app-client.ts +6 -3
- package/src/commands/analyze/clusters.ts +29 -27
- package/src/commands/analyze/colors.ts +22 -6
- package/src/commands/analyze/index.ts +2 -2
- package/src/commands/analyze/spacing.ts +11 -4
- package/src/commands/analyze/typography.ts +24 -8
- package/src/commands/convert.ts +56 -0
- package/src/commands/eval.ts +18 -8
- package/src/commands/export.ts +106 -49
- package/src/commands/find.ts +20 -22
- package/src/commands/formats.ts +80 -0
- package/src/commands/info.ts +8 -3
- package/src/commands/lint.ts +86 -0
- package/src/commands/node.ts +15 -6
- package/src/commands/pages.ts +9 -4
- package/src/commands/query.ts +12 -28
- package/src/commands/tree.ts +12 -4
- package/src/commands/variables.ts +12 -4
- package/src/format.ts +67 -6
- package/src/headless.ts +8 -26
- package/src/index.ts +8 -2
package/src/commands/find.ts
CHANGED
|
@@ -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(
|
|
11
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
+
})
|
package/src/commands/info.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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: {
|
|
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
|
+
})
|
package/src/commands/node.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
7
|
+
import { loadDocument } from '../headless'
|
|
7
8
|
|
|
8
9
|
import type { Color, NodeResult } from '@open-pencil/core'
|
|
9
10
|
|
|
10
|
-
async function getData(
|
|
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: {
|
|
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 = (
|
|
51
|
-
.
|
|
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
|
package/src/commands/pages.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
18
|
+
meta: { description: 'List pages in a document' },
|
|
18
19
|
args: {
|
|
19
|
-
file: {
|
|
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 }) {
|
package/src/commands/query.ts
CHANGED
|
@@ -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"
|
|
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: '
|
|
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
|
-
|
|
70
|
-
|
|
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
|
})
|
package/src/commands/tree.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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(
|
|
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: {
|
|
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 {
|
|
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 {
|
|
7
|
+
import { loadDocument } from '../headless'
|
|
7
8
|
|
|
8
9
|
import type { VariablesResult } from '@open-pencil/core'
|
|
9
10
|
|
|
10
|
-
async function getData(
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
type SceneGraph,
|
|
5
|
-
type ExportFormat,
|
|
2
|
+
BUILTIN_IO_FORMATS,
|
|
3
|
+
IORegistry,
|
|
6
4
|
computeAllLayouts,
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
15
|
-
const graph = await
|
|
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
|
|
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
|
|
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,
|