@penpair/core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -2
- package/dist/baselines-config.d.ts +19 -0
- package/dist/baselines-config.js +31 -0
- package/dist/baselines.d.ts +72 -0
- package/dist/baselines.js +122 -0
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +466 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/client.d.ts +37 -0
- package/dist/client.js +119 -0
- package/dist/deep-check.d.ts +32 -0
- package/dist/deep-check.js +93 -0
- package/dist/docker/lifecycle.d.ts +96 -0
- package/dist/docker/lifecycle.js +273 -0
- package/dist/docker/secrets.d.ts +11 -0
- package/dist/docker/secrets.js +38 -0
- package/dist/docker/template.d.ts +8 -0
- package/dist/docker/template.js +158 -0
- package/dist/docker-profile.d.ts +114 -0
- package/dist/docker-profile.js +18 -0
- package/dist/endpoint.d.ts +36 -0
- package/dist/endpoint.js +130 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +21 -0
- package/dist/export.d.ts +21 -0
- package/dist/export.js +40 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +23 -0
- package/dist/instance/config.d.ts +49 -0
- package/dist/instance/config.js +103 -0
- package/dist/instance/connect.d.ts +63 -0
- package/dist/instance/connect.js +158 -0
- package/dist/instance/connection.d.ts +36 -0
- package/dist/instance/connection.js +66 -0
- package/dist/instance/gitignore.d.ts +7 -0
- package/dist/instance/gitignore.js +19 -0
- package/dist/instance/init.d.ts +31 -0
- package/dist/instance/init.js +32 -0
- package/dist/instance/lifecycle.d.ts +129 -0
- package/dist/instance/lifecycle.js +336 -0
- package/dist/instance/runner.d.ts +18 -0
- package/dist/instance/runner.js +36 -0
- package/dist/instance/secrets.d.ts +11 -0
- package/dist/instance/secrets.js +38 -0
- package/dist/instance/setup.d.ts +40 -0
- package/dist/instance/setup.js +56 -0
- package/dist/instance/template.d.ts +8 -0
- package/dist/instance/template.js +158 -0
- package/dist/instance/tty.d.ts +5 -0
- package/dist/instance/tty.js +64 -0
- package/dist/local-profile.d.ts +95 -0
- package/dist/local-profile.js +8 -0
- package/dist/operation.d.ts +41 -0
- package/dist/operation.js +55 -0
- package/dist/probe.d.ts +21 -0
- package/dist/probe.js +81 -0
- package/dist/project-snippets.d.ts +14 -0
- package/dist/project-snippets.js +68 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.js +220 -0
- package/dist/skills.d.ts +32 -0
- package/dist/skills.js +91 -0
- package/dist/snippet.d.ts +24 -0
- package/dist/snippet.js +37 -0
- package/dist/snippets/archive.d.ts +18 -0
- package/dist/snippets/archive.js +41 -0
- package/dist/snippets/read.d.ts +32 -0
- package/dist/snippets/read.js +65 -0
- package/dist/tokens-diff.d.ts +29 -0
- package/dist/tokens-diff.js +27 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +50 -3
- package/skills/design-judgment/SKILL.md +50 -0
- package/skills/design-judgment/references/critique-and-evidence.md +39 -0
- package/skills/design-judgment/references/decision-rules.md +58 -0
- package/skills/design-judgment/references/recipes/admin-queue.md +33 -0
- package/skills/design-judgment/references/recipes/list-detail.md +35 -0
- package/skills/design-judgment/references/recipes/status-and-feedback.md +37 -0
- package/skills/design-judgment/references/sources.md +31 -0
- package/skills/penpair-setup/SKILL.md +53 -0
- package/skills/penpair-setup/references/web-or-docker.md +35 -0
- package/skills/penpot-design/SKILL.md +53 -0
- package/skills/penpot-design/references/auto-layout.md +31 -0
- package/skills/penpot-design/references/library-in-penpot.md +34 -0
- package/skills/penpot-design/references/penpot-plugin-gotchas.md +50 -0
- package/skills/penpot-design/references/project-facts.md +24 -0
- package/skills/penpot-design/references/write-protocol.md +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niklas Westman
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @penpair/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The library and engine behind [penpair](https://github.com/niklas-westman/penpair): connect AI agents and people to a Penpot design file, safely.**
|
|
4
|
+
|
|
5
|
+
This package has no command of its own. For the command, install one of:
|
|
6
|
+
|
|
7
|
+
- [`@penpair/web`](https://www.npmjs.com/package/@penpair/web): Penpot on the web (design.penpot.app or your team's own). The default.
|
|
8
|
+
- [`@penpair/docker`](https://www.npmjs.com/package/@penpair/docker): the same, plus Penpot in Docker on your computer.
|
|
9
|
+
|
|
10
|
+
Both include everything in this package. Install this one directly only to use the library from your own code: the connection, the safe write runner, the snippet registry, `defineSnippet`, the skills and `runCli`.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineSnippet } from "@penpair/core";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The guide is in the [repository](https://github.com/niklas-westman/penpair#readme).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** One frame of a Penpot page that becomes an approved reference image. */
|
|
2
|
+
export interface BaselineFrame {
|
|
3
|
+
/** The exact name of a top-level frame on the page. */
|
|
4
|
+
frame: string;
|
|
5
|
+
/** Names the image file (`<id>.png`) and the check that uses it. Lowercase letters, digits, hyphens and underscores. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** The route in the implementation this frame is the design of. Frames without one are exported but not given a check. */
|
|
8
|
+
route?: string;
|
|
9
|
+
}
|
|
10
|
+
/** What `penpair baselines` exports: the frames of one page, as images in a folder of the project. */
|
|
11
|
+
export interface BaselinesConfig {
|
|
12
|
+
page: string;
|
|
13
|
+
/** Folder for the images, relative to the project. */
|
|
14
|
+
root: string;
|
|
15
|
+
frames: BaselineFrame[];
|
|
16
|
+
/** Allowed share of differing pixels in the generated checks (default 0.05). */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function validateBaselines(value: unknown, fail: (message: string) => never): BaselinesConfig;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const idPattern = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
3
|
+
export function validateBaselines(value, fail) {
|
|
4
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
5
|
+
return fail("baselines must be an object");
|
|
6
|
+
const v = value;
|
|
7
|
+
if (typeof v.page !== "string" || v.page.length === 0)
|
|
8
|
+
fail("baselines.page must be the name of a page");
|
|
9
|
+
if (typeof v.root !== "string" || v.root.length === 0 || path.isAbsolute(v.root) || v.root.split(/[\\/]/).includes(".."))
|
|
10
|
+
fail("baselines.root must be a relative folder that stays inside the project");
|
|
11
|
+
if (!Array.isArray(v.frames) || v.frames.length === 0)
|
|
12
|
+
fail("baselines.frames must list at least one frame");
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (const entry of v.frames) {
|
|
15
|
+
const f = entry;
|
|
16
|
+
if (typeof f !== "object" || f === null)
|
|
17
|
+
fail("every entry in baselines.frames must be an object");
|
|
18
|
+
if (typeof f.frame !== "string" || f.frame.length === 0)
|
|
19
|
+
fail("every baselines.frames entry needs a frame name");
|
|
20
|
+
if (typeof f.id !== "string" || !idPattern.test(f.id))
|
|
21
|
+
fail(`baselines.frames id ${JSON.stringify(f.id)} must be lowercase letters, digits, hyphens or underscores`);
|
|
22
|
+
if (seen.has(f.id))
|
|
23
|
+
fail(`baselines.frames id ${JSON.stringify(f.id)} is used twice`);
|
|
24
|
+
seen.add(f.id);
|
|
25
|
+
if (f.route !== undefined && (typeof f.route !== "string" || f.route.length === 0))
|
|
26
|
+
fail("baselines.frames route must be a non-empty string");
|
|
27
|
+
}
|
|
28
|
+
if (v.threshold !== undefined && (typeof v.threshold !== "number" || v.threshold < 0 || v.threshold > 1))
|
|
29
|
+
fail("baselines.threshold must be a number from 0 to 1");
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PenpotToolClient } from "./client.js";
|
|
2
|
+
import type { BaselinesConfig } from "./baselines-config.js";
|
|
3
|
+
import { type ExportOptions } from "./export.js";
|
|
4
|
+
export * from "./baselines-config.js";
|
|
5
|
+
export type BaselineStatus =
|
|
6
|
+
/** No image in the folder yet. */
|
|
7
|
+
"new"
|
|
8
|
+
/** The image in the folder is byte-identical to the design now. */
|
|
9
|
+
| "same"
|
|
10
|
+
/** The design differs from the image in the folder. Replaced only with `update`. */
|
|
11
|
+
| "changed"
|
|
12
|
+
/** The export never settled (two identical renders in a row), so it is not trusted. */
|
|
13
|
+
| "unstable"
|
|
14
|
+
/** The page has no top-level frame with that name. */
|
|
15
|
+
| "missing";
|
|
16
|
+
export interface BaselineResult {
|
|
17
|
+
id: string;
|
|
18
|
+
frame: string;
|
|
19
|
+
frameId?: string;
|
|
20
|
+
width?: number;
|
|
21
|
+
height?: number;
|
|
22
|
+
file: string;
|
|
23
|
+
status: BaselineStatus;
|
|
24
|
+
/** Whether the image was written to the folder in this run. */
|
|
25
|
+
written: boolean;
|
|
26
|
+
/** The image now in the folder: its hash, size in bytes and pixel size. For a design that changed and was not updated this is the old image. */
|
|
27
|
+
sha256?: string;
|
|
28
|
+
bytes?: number;
|
|
29
|
+
imageWidth?: number;
|
|
30
|
+
imageHeight?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface BaselinesReport {
|
|
33
|
+
page: string;
|
|
34
|
+
penpotFile: string;
|
|
35
|
+
revn: number;
|
|
36
|
+
results: BaselineResult[];
|
|
37
|
+
/** Files written: images, the manifest and the generated checks. */
|
|
38
|
+
wrote: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface BaselinesOptions extends ExportOptions {
|
|
41
|
+
/** Write images that are new. Without it nothing is written, whatever is found. */
|
|
42
|
+
apply?: boolean;
|
|
43
|
+
/** Also replace images whose design has changed. This is the act of approving the new design as the reference; it is the owner's call. */
|
|
44
|
+
update?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Pixel size from a PNG's header (the IHDR chunk), or nothing when the bytes are not a PNG. */
|
|
47
|
+
export declare function pngSize(buffer: Buffer): {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
} | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Exports the configured frames of a page as images into the project's folder: the approved design that an implementation is
|
|
53
|
+
* checked against. Looks first; writes only with `apply`, and replaces an image that differs only with `update`, so a change of
|
|
54
|
+
* the design never silently becomes the new reference.
|
|
55
|
+
*/
|
|
56
|
+
export declare function exportBaselines(client: PenpotToolClient, projectRoot: string, config: BaselinesConfig, options?: BaselinesOptions): Promise<BaselinesReport>;
|
|
57
|
+
/**
|
|
58
|
+
* Visual cases for Reflection, one per frame that names a route: the image's exact size is the viewport (so the capture and the
|
|
59
|
+
* reference have the same dimensions), the image is the baseline, and `ref` joins the check back to the frame.
|
|
60
|
+
* Review-only (`strict: false`): a difference is shown to a person, not failed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function reflectionCases(config: BaselinesConfig, results: BaselineResult[]): {
|
|
63
|
+
note: string;
|
|
64
|
+
routes: Record<string, {
|
|
65
|
+
viewports: {
|
|
66
|
+
name: string;
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
}[];
|
|
70
|
+
}>;
|
|
71
|
+
visualSmoke: Record<string, unknown>[];
|
|
72
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { exportShapePng } from "./export.js";
|
|
5
|
+
import { runOnPage } from "./operation.js";
|
|
6
|
+
import { frames } from "./snippets/read.js";
|
|
7
|
+
export * from "./baselines-config.js";
|
|
8
|
+
const sha = (buffer) => createHash("sha256").update(buffer).digest("hex");
|
|
9
|
+
/** Pixel size from a PNG's header (the IHDR chunk), or nothing when the bytes are not a PNG. */
|
|
10
|
+
export function pngSize(buffer) {
|
|
11
|
+
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
12
|
+
if (buffer.length < 24 || !buffer.subarray(0, 8).equals(signature) || buffer.subarray(12, 16).toString("ascii") !== "IHDR")
|
|
13
|
+
return undefined;
|
|
14
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
15
|
+
}
|
|
16
|
+
async function readIfExists(file) {
|
|
17
|
+
try {
|
|
18
|
+
return await readFile(file);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Exports the configured frames of a page as images into the project's folder: the approved design that an implementation is
|
|
26
|
+
* checked against. Looks first; writes only with `apply`, and replaces an image that differs only with `update`, so a change of
|
|
27
|
+
* the design never silently becomes the new reference.
|
|
28
|
+
*/
|
|
29
|
+
export async function exportBaselines(client, projectRoot, config, options = {}) {
|
|
30
|
+
const listing = await runOnPage(client, frames({ page: config.page }), config.page, options);
|
|
31
|
+
const found = listing.frames ?? [];
|
|
32
|
+
const results = [];
|
|
33
|
+
const wrote = [];
|
|
34
|
+
const folder = path.join(projectRoot, config.root);
|
|
35
|
+
for (const wanted of config.frames) {
|
|
36
|
+
const file = `${wanted.id}.png`;
|
|
37
|
+
const matches = found.filter((f) => f.name === wanted.frame);
|
|
38
|
+
if (matches.length === 0) {
|
|
39
|
+
results.push({ id: wanted.id, frame: wanted.frame, file, status: "missing", written: false });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (matches.length > 1)
|
|
43
|
+
throw new Error(`more than one top-level frame on ${config.page} is named ${JSON.stringify(wanted.frame)}; rename one so the design reference is unambiguous`);
|
|
44
|
+
const frame = matches[0];
|
|
45
|
+
options.onStep?.(`exporting ${wanted.frame}`);
|
|
46
|
+
const exported = await exportShapePng(client, frame.id, { ...options, page: config.page, stable: true });
|
|
47
|
+
const base = { id: wanted.id, frame: wanted.frame, frameId: frame.id, width: frame.width, height: frame.height, file };
|
|
48
|
+
if (!exported.stable) {
|
|
49
|
+
results.push({ ...base, status: "unstable", written: false });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const target = path.join(folder, file);
|
|
53
|
+
const existing = await readIfExists(target);
|
|
54
|
+
const status = existing === undefined ? "new" : sha(existing) === sha(exported.png) ? "same" : "changed";
|
|
55
|
+
let written = false;
|
|
56
|
+
if (options.apply && (status === "new" || (status === "changed" && options.update))) {
|
|
57
|
+
await mkdir(folder, { recursive: true });
|
|
58
|
+
await writeFile(target, exported.png);
|
|
59
|
+
written = true;
|
|
60
|
+
wrote.push(path.join(config.root, file));
|
|
61
|
+
}
|
|
62
|
+
// A new image that was not written (a look, without --apply) is described by the export; a changed one that was not updated by what is on disk.
|
|
63
|
+
const onDisk = written || status === "same" || status === "new" ? exported.png : existing;
|
|
64
|
+
const size = pngSize(onDisk);
|
|
65
|
+
results.push({ ...base, status, written, sha256: sha(onDisk), bytes: onDisk.length, imageWidth: size?.width, imageHeight: size?.height });
|
|
66
|
+
}
|
|
67
|
+
if (options.apply) {
|
|
68
|
+
const onDisk = results.filter((r) => r.status !== "missing" && r.status !== "unstable" && (r.written || r.status === "same" || r.status === "changed"));
|
|
69
|
+
if (onDisk.length > 0) {
|
|
70
|
+
await mkdir(folder, { recursive: true });
|
|
71
|
+
const manifest = path.join(folder, "penpair-baselines.json");
|
|
72
|
+
await writeFile(manifest, `${JSON.stringify(manifestOf(config, listing, onDisk), null, 2)}\n`);
|
|
73
|
+
wrote.push(path.join(config.root, "penpair-baselines.json"));
|
|
74
|
+
const cases = path.join(folder, "reflection-cases.json");
|
|
75
|
+
await writeFile(cases, `${JSON.stringify(reflectionCases(config, onDisk), null, 2)}\n`);
|
|
76
|
+
wrote.push(path.join(config.root, "reflection-cases.json"));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { page: config.page, penpotFile: String(listing.file ?? ""), revn: Number(listing.revn ?? 0), results, wrote };
|
|
80
|
+
}
|
|
81
|
+
function manifestOf(config, listing, results) {
|
|
82
|
+
return {
|
|
83
|
+
note: "Written by `penpair baselines`. The images are the approved design for each frame; change one only on purpose (`penpair baselines --apply --update`).",
|
|
84
|
+
penpotFile: listing.file,
|
|
85
|
+
page: config.page,
|
|
86
|
+
revn: listing.revn,
|
|
87
|
+
frames: results.map((r) => ({ id: r.id, frame: r.frame, frameId: r.frameId, width: r.imageWidth, height: r.imageHeight, file: r.file, sha256: r.sha256, bytes: r.bytes })),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Visual cases for Reflection, one per frame that names a route: the image's exact size is the viewport (so the capture and the
|
|
92
|
+
* reference have the same dimensions), the image is the baseline, and `ref` joins the check back to the frame.
|
|
93
|
+
* Review-only (`strict: false`): a difference is shown to a person, not failed.
|
|
94
|
+
*/
|
|
95
|
+
export function reflectionCases(config, results) {
|
|
96
|
+
const routes = {};
|
|
97
|
+
const visualSmoke = [];
|
|
98
|
+
for (const wanted of config.frames) {
|
|
99
|
+
const result = results.find((r) => r.id === wanted.id);
|
|
100
|
+
if (!wanted.route || !result || result.imageWidth === undefined || result.imageHeight === undefined)
|
|
101
|
+
continue;
|
|
102
|
+
const viewport = { name: `design-${result.imageWidth}x${result.imageHeight}`, width: result.imageWidth, height: result.imageHeight };
|
|
103
|
+
const entry = (routes[wanted.route] ??= { viewports: [] });
|
|
104
|
+
if (!entry.viewports.some((v) => v.name === viewport.name))
|
|
105
|
+
entry.viewports.push(viewport);
|
|
106
|
+
visualSmoke.push({
|
|
107
|
+
id: wanted.id,
|
|
108
|
+
route: wanted.route,
|
|
109
|
+
viewport: viewport.name,
|
|
110
|
+
baselineRoot: config.root,
|
|
111
|
+
baseline: `${wanted.id}.png`,
|
|
112
|
+
threshold: { maxDiffPixelRatio: config.threshold ?? 0.05 },
|
|
113
|
+
strict: false,
|
|
114
|
+
ref: `penpair:${wanted.id}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
note: "Generated by `penpair baselines`. Add each route's viewports to that route in your Reflection config and spread `visualSmoke` into the browser contract; baselineRoot is relative to the Reflection config's folder.",
|
|
119
|
+
routes,
|
|
120
|
+
visualSmoke,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LocalProfile } from "./local-profile.js";
|
|
2
|
+
export interface CliOptions {
|
|
3
|
+
/** A way to run Penpot on this computer, from a package that provides one (`@penpair/docker`). Without it those commands explain where they come from. */
|
|
4
|
+
local?: LocalProfile;
|
|
5
|
+
}
|
|
6
|
+
/** Runs the command line and returns the exit code. The `penpair` command calls it with nothing extra; a package that adds a way to run Penpot calls it with its profile. */
|
|
7
|
+
export declare function runCli(argv: string[], options?: CliOptions): Promise<number>;
|