@penpair/core 0.1.1 → 0.1.2
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/dist/adapters.d.ts +49 -0
- package/dist/adapters.js +71 -0
- package/dist/cli-main.d.ts +3 -0
- package/dist/cli-main.js +46 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/instance/config.d.ts +3 -0
- package/dist/instance/config.js +4 -0
- package/dist/instance/setup.js +1 -1
- package/dist/skills.d.ts +8 -0
- package/dist/skills.js +16 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/skills/design-judgment/SKILL.md +1 -1
- package/skills/{penpot-design → penpair-design}/SKILL.md +2 -2
- package/skills/{penpot-design → penpair-design}/references/penpot-plugin-gotchas.md +2 -0
- package/skills/penpair-setup/SKILL.md +2 -2
- /package/skills/{penpot-design → penpair-design}/references/auto-layout.md +0 -0
- /package/skills/{penpot-design → penpair-design}/references/library-in-penpot.md +0 -0
- /package/skills/{penpot-design → penpair-design}/references/project-facts.md +0 -0
- /package/skills/{penpot-design → penpair-design}/references/write-protocol.md +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { PenpotToolClient } from "./client.js";
|
|
2
|
+
import { type ProjectConfig } from "./instance/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* An adapter is a package that teaches penpair about one library or tool (a component library's tokens, a checker) by adding
|
|
5
|
+
* commands. A project lists the adapters it uses under `adapters` in penpair.config.json; the core loads them by name from the
|
|
6
|
+
* project and knows nothing about any of them. An adapter keeps its own settings in its own block of the same file.
|
|
7
|
+
*/
|
|
8
|
+
export interface AdapterContext {
|
|
9
|
+
/** The project's folder. */
|
|
10
|
+
root: string;
|
|
11
|
+
/** The project's validated configuration. */
|
|
12
|
+
config: ProjectConfig;
|
|
13
|
+
/** The whole penpair.config.json as written, for the adapter's own block. */
|
|
14
|
+
rawConfig: Record<string, unknown>;
|
|
15
|
+
log(message: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Connects to the project's Penpot and runs `work` with the connection, closing it afterwards. The endpoint must belong to this
|
|
18
|
+
* project's Penpot, as for every command. Not called by commands that work offline.
|
|
19
|
+
*/
|
|
20
|
+
withPenpot<T>(work: (client: PenpotToolClient) => Promise<T>): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export interface AdapterCommand {
|
|
23
|
+
/** The word after `penpair`, for example `hello`. It must not be a command the core already has. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** One line for `penpair --help`. */
|
|
26
|
+
summary: string;
|
|
27
|
+
/** Lines of usage for `penpair --help`. */
|
|
28
|
+
usage: string[];
|
|
29
|
+
/** Runs with the arguments after the command's name; returns the exit code. The adapter parses its own flags. */
|
|
30
|
+
run(context: AdapterContext, args: string[]): Promise<number>;
|
|
31
|
+
}
|
|
32
|
+
export interface Adapter {
|
|
33
|
+
name: string;
|
|
34
|
+
commands: AdapterCommand[];
|
|
35
|
+
}
|
|
36
|
+
/** Declares an adapter. Returns the definition unchanged, so a module can `export default defineAdapter({...})` with types checked. */
|
|
37
|
+
export declare const defineAdapter: <T extends Adapter>(adapter: T) => T;
|
|
38
|
+
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
39
|
+
export declare const reservedCommands: readonly ["probe", "pages", "tree", "audit", "tokens", "export", "archive", "snippets", "run", "skills", "init", "setup", "connect", "up", "down", "status", "doctor", "backup", "restore", "baselines"];
|
|
40
|
+
export interface LoadedAdapters {
|
|
41
|
+
adapters: Adapter[];
|
|
42
|
+
problems: string[];
|
|
43
|
+
}
|
|
44
|
+
export type AdapterImporter = (specifier: string, root: string) => Promise<unknown>;
|
|
45
|
+
/** Resolves an adapter the way the project would: a relative path from the project's folder, or a package installed in the project. */
|
|
46
|
+
export declare const importFromProject: AdapterImporter;
|
|
47
|
+
export declare function loadAdapters(root: string, specifiers: string[] | undefined, importer?: AdapterImporter): Promise<LoadedAdapters>;
|
|
48
|
+
/** The whole penpair.config.json as written (an adapter's own settings live in it). Empty when there is no file. */
|
|
49
|
+
export declare function readRawConfig(root: string): Promise<Record<string, unknown>>;
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { configFileName } from "./instance/config.js";
|
|
6
|
+
/** Declares an adapter. Returns the definition unchanged, so a module can `export default defineAdapter({...})` with types checked. */
|
|
7
|
+
export const defineAdapter = (adapter) => adapter;
|
|
8
|
+
/** The commands of the core itself; an adapter cannot take one of these names. */
|
|
9
|
+
export const reservedCommands = [
|
|
10
|
+
"probe", "pages", "tree", "audit", "tokens", "export", "archive", "snippets", "run", "skills", "init", "setup", "connect",
|
|
11
|
+
"up", "down", "status", "doctor", "backup", "restore", "baselines",
|
|
12
|
+
];
|
|
13
|
+
/** Resolves an adapter the way the project would: a relative path from the project's folder, or a package installed in the project. */
|
|
14
|
+
export const importFromProject = async (specifier, root) => {
|
|
15
|
+
let file;
|
|
16
|
+
if (specifier.startsWith(".") || path.isAbsolute(specifier))
|
|
17
|
+
file = path.resolve(root, specifier);
|
|
18
|
+
else {
|
|
19
|
+
try {
|
|
20
|
+
file = createRequire(path.join(root, "package.json")).resolve(specifier);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
const error = Object.assign(new Error(`${specifier} is not installed in this project`), { code: "ERR_MODULE_NOT_FOUND" });
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return import(pathToFileURL(file).href);
|
|
28
|
+
};
|
|
29
|
+
export async function loadAdapters(root, specifiers, importer = importFromProject) {
|
|
30
|
+
const adapters = [];
|
|
31
|
+
const problems = [];
|
|
32
|
+
const taken = new Map();
|
|
33
|
+
for (const specifier of specifiers ?? []) {
|
|
34
|
+
let loaded;
|
|
35
|
+
try {
|
|
36
|
+
loaded = (await importer(specifier, root));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = error.code;
|
|
40
|
+
problems.push(code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"
|
|
41
|
+
? `adapter ${specifier} is not installed in this project. Install it (for example \`npm i -D ${specifier}\`).`
|
|
42
|
+
: `adapter ${specifier} could not be loaded: ${String(error.message).split("\n")[0]}`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const adapter = loaded.default;
|
|
46
|
+
if (!adapter || typeof adapter.name !== "string" || !Array.isArray(adapter.commands)) {
|
|
47
|
+
problems.push(`adapter ${specifier} does not export a default adapter (use \`export default defineAdapter({...})\`)`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const command of adapter.commands) {
|
|
51
|
+
if (reservedCommands.includes(command.name))
|
|
52
|
+
problems.push(`adapter ${specifier}: the command "${command.name}" is one the core already has`);
|
|
53
|
+
else if (taken.has(command.name))
|
|
54
|
+
problems.push(`adapter ${specifier}: the command "${command.name}" is already provided by ${taken.get(command.name)}`);
|
|
55
|
+
else
|
|
56
|
+
taken.set(command.name, adapter.name);
|
|
57
|
+
}
|
|
58
|
+
adapters.push({ ...adapter, commands: adapter.commands.filter((c) => taken.get(c.name) === adapter.name) });
|
|
59
|
+
}
|
|
60
|
+
return { adapters, problems };
|
|
61
|
+
}
|
|
62
|
+
/** The whole penpair.config.json as written (an adapter's own settings live in it). Empty when there is no file. */
|
|
63
|
+
export async function readRawConfig(root) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(await readFile(path.join(root, configFileName), "utf8"));
|
|
66
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/cli-main.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type Adapter } from "./adapters.js";
|
|
1
2
|
import { type LocalProfile } from "./local-profile.js";
|
|
2
3
|
export interface CliOptions {
|
|
4
|
+
/** Adapters already loaded (the command loads the project's own from penpair.config.json). */
|
|
5
|
+
adapters?: Adapter[];
|
|
3
6
|
/** 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
7
|
local?: LocalProfile;
|
|
5
8
|
}
|
package/dist/cli-main.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
4
|
import { connect, executeCode } from "./client.js";
|
|
4
5
|
import { redactEndpoint, resolveEndpoint } from "./endpoint.js";
|
|
@@ -19,6 +20,7 @@ import { init } from "./instance/init.js";
|
|
|
19
20
|
import { ConnectError, connectProject, keyFileCheck } from "./instance/connect.js";
|
|
20
21
|
import { hostedOrigin, setup } from "./instance/setup.js";
|
|
21
22
|
import { askHidden, askLine, openInBrowser } from "./instance/tty.js";
|
|
23
|
+
import { loadAdapters, readRawConfig, reservedCommands } from "./adapters.js";
|
|
22
24
|
import { requireLocalProfile } from "./local-profile.js";
|
|
23
25
|
import { assertEndpointIsInstance, doctorExternal, LifecycleError } from "./instance/connection.js";
|
|
24
26
|
import { nodeRunner } from "./instance/runner.js";
|
|
@@ -67,7 +69,9 @@ const usageConnection = `Connecting to a Penpot (one project, configured in penp
|
|
|
67
69
|
penpair init --external <origin> use a Penpot someone else runs (a team server or https://design.penpot.app)
|
|
68
70
|
penpair doctor [--json] what is up, what is not, and what to do about it (--json for scripts and agents)
|
|
69
71
|
`;
|
|
70
|
-
const usage = (hasLocal) => usageHead.replace("@@INSTANCE@@", usageConnection + (hasLocal ? usageLocalInstance : usageNoLocalInstance)).replace("@@SETUP@@", hasLocal ? " penpair setup --docker Penpot on this computer in Docker\n" : "");
|
|
72
|
+
const usage = (hasLocal, adapters = []) => usageHead.replace("@@INSTANCE@@", usageConnection + (hasLocal ? usageLocalInstance : usageNoLocalInstance)).replace("@@SETUP@@", hasLocal ? " penpair setup --docker Penpot on this computer in Docker\n" : "") + adapterUsage(adapters);
|
|
73
|
+
/** The commands that adapters from penpair.config.json add, after the core's own. */
|
|
74
|
+
const adapterUsage = (adapters) => adapters.length === 0 ? "" : `\nAdapters (listed under "adapters" in penpair.config.json):\n${adapters.flatMap((a) => a.commands.flatMap((c) => c.usage.map((line) => ` ${line}`))).join("\n")}\n`;
|
|
71
75
|
const list = (value) => (value ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
72
76
|
async function main(argv, cli) {
|
|
73
77
|
const { values, positionals } = parseArgs({
|
|
@@ -113,7 +117,7 @@ async function main(argv, cli) {
|
|
|
113
117
|
return 0;
|
|
114
118
|
}
|
|
115
119
|
if (!command || values.help) {
|
|
116
|
-
console.log(usage(Boolean(cli.local)));
|
|
120
|
+
console.log(usage(Boolean(cli.local), cli.adapters));
|
|
117
121
|
return 0;
|
|
118
122
|
}
|
|
119
123
|
if (command === "tokens" && rest[0] === "diff") {
|
|
@@ -220,7 +224,9 @@ async function main(argv, cli) {
|
|
|
220
224
|
console.log(`linked ${link}`);
|
|
221
225
|
console.log(`${report.unchanged.length} file(s) already up to date`);
|
|
222
226
|
console.log("\nAdd to your AGENTS.md skills table:\n" + report.agentsRows.join("\n"));
|
|
223
|
-
|
|
227
|
+
for (const old of report.legacy)
|
|
228
|
+
console.log(`\nnote ${old.folder} is the old name of ${old.renamedTo}. ${old.carriedFacts ? "Your project-facts.md was copied to the new folder. " : ""}Delete the old folder and its links (.agents/skills/${path.posix.basename(old.folder)}, .claude/skills/${path.posix.basename(old.folder)}) and update its row in your AGENTS.md.`);
|
|
229
|
+
console.log("\nThen fill in references/project-facts.md in penpair-design: it is yours and is never replaced.");
|
|
224
230
|
return 0;
|
|
225
231
|
}
|
|
226
232
|
for (const name of await listBundledSkills())
|
|
@@ -445,7 +451,7 @@ async function main(argv, cli) {
|
|
|
445
451
|
return report.phase === "applied" && !report.verified ? 1 : 0;
|
|
446
452
|
}
|
|
447
453
|
else {
|
|
448
|
-
console.log(usage(Boolean(cli.local)));
|
|
454
|
+
console.log(usage(Boolean(cli.local), cli.adapters));
|
|
449
455
|
return 1;
|
|
450
456
|
}
|
|
451
457
|
return 0;
|
|
@@ -454,10 +460,45 @@ async function main(argv, cli) {
|
|
|
454
460
|
await client.close();
|
|
455
461
|
}
|
|
456
462
|
}
|
|
463
|
+
/** Runs one command of an adapter with a context that can reach the project's Penpot. */
|
|
464
|
+
async function runAdapterCommand(command, root, args) {
|
|
465
|
+
const config = await loadConfig(root);
|
|
466
|
+
const rawConfig = await readRawConfig(root);
|
|
467
|
+
const context = {
|
|
468
|
+
root,
|
|
469
|
+
config,
|
|
470
|
+
rawConfig,
|
|
471
|
+
log: (message) => console.error(`… ${message}`),
|
|
472
|
+
async withPenpot(work) {
|
|
473
|
+
const endpoint = await resolveEndpoint();
|
|
474
|
+
assertEndpointIsInstance(endpoint.description, config);
|
|
475
|
+
const client = await connect(endpoint);
|
|
476
|
+
try {
|
|
477
|
+
return await work(client);
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
await client.close();
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
return command.run(context, args);
|
|
485
|
+
}
|
|
457
486
|
/** 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. */
|
|
458
487
|
export async function runCli(argv, options = {}) {
|
|
459
488
|
try {
|
|
460
|
-
|
|
489
|
+
const root = process.cwd();
|
|
490
|
+
const raw = await readRawConfig(root);
|
|
491
|
+
const specifiers = Array.isArray(raw.adapters) ? raw.adapters.filter((entry) => typeof entry === "string") : [];
|
|
492
|
+
const loaded = options.adapters ? { adapters: options.adapters, problems: [] } : specifiers.length > 0 ? await loadAdapters(root, specifiers) : { adapters: [], problems: [] };
|
|
493
|
+
const [first] = argv;
|
|
494
|
+
const adapterCommand = loaded.adapters.flatMap((adapter) => adapter.commands).find((command) => command.name === first);
|
|
495
|
+
if (adapterCommand)
|
|
496
|
+
return await runAdapterCommand(adapterCommand, root, argv.slice(1));
|
|
497
|
+
const builtIn = reservedCommands.includes(first ?? "");
|
|
498
|
+
if (loaded.problems.length > 0 && (!builtIn || argv.includes("--help")))
|
|
499
|
+
for (const problem of loaded.problems)
|
|
500
|
+
console.error(`warning: ${problem}`);
|
|
501
|
+
return await main(argv, { ...options, adapters: loaded.adapters });
|
|
461
502
|
}
|
|
462
503
|
catch (error) {
|
|
463
504
|
console.error(error instanceof PenpairError ? error.message : error instanceof ConfigError || error instanceof ConnectError || error instanceof LifecycleError || error instanceof RegistryError ? `penpair: ${error.message}` : `penpair: ${String(error?.message ?? error)}`);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@ export interface InstanceConfig {
|
|
|
17
17
|
snippets?: string[];
|
|
18
18
|
/** Frames of a page to export as approved reference images for a check (see `penpair baselines`). */
|
|
19
19
|
baselines?: BaselinesConfig;
|
|
20
|
+
/** Packages (or relative paths) that add commands for a library or tool; each keeps its own settings in its own block of this file. */
|
|
21
|
+
adapters?: string[];
|
|
20
22
|
}
|
|
21
23
|
/** A project that uses a Penpot someone else runs (a team server or the hosted service): no Docker here, only the connection. */
|
|
22
24
|
export interface ExternalConfig {
|
|
@@ -27,6 +29,7 @@ export interface ExternalConfig {
|
|
|
27
29
|
};
|
|
28
30
|
snippets?: string[];
|
|
29
31
|
baselines?: BaselinesConfig;
|
|
32
|
+
adapters?: string[];
|
|
30
33
|
}
|
|
31
34
|
export type ProjectConfig = InstanceConfig | ExternalConfig;
|
|
32
35
|
export declare const isExternal: (config: ProjectConfig) => config is ExternalConfig;
|
package/dist/instance/config.js
CHANGED
|
@@ -39,6 +39,8 @@ export function validateConfig(value) {
|
|
|
39
39
|
}
|
|
40
40
|
if (v.baselines !== undefined)
|
|
41
41
|
validateBaselines(v.baselines, fail);
|
|
42
|
+
if (v.adapters !== undefined && !(Array.isArray(v.adapters) && v.adapters.every((e) => typeof e === "string" && e.length > 0)))
|
|
43
|
+
fail("adapters must be a list of package names or relative paths");
|
|
42
44
|
return v;
|
|
43
45
|
}
|
|
44
46
|
const instanceNamePattern = /^[a-z][a-z0-9-]{0,30}$/;
|
|
@@ -67,6 +69,8 @@ export function validateProjectConfig(value) {
|
|
|
67
69
|
fail("snippets must be a list of relative paths that stay inside the project");
|
|
68
70
|
if (v.baselines !== undefined)
|
|
69
71
|
validateBaselines(v.baselines, fail);
|
|
72
|
+
if (v.adapters !== undefined && !(Array.isArray(v.adapters) && v.adapters.every((e) => typeof e === "string" && e.length > 0)))
|
|
73
|
+
fail("adapters must be a list of package names or relative paths");
|
|
70
74
|
return v;
|
|
71
75
|
}
|
|
72
76
|
/** The config of a project that runs its own Penpot; anything else cannot start, stop or back up containers. */
|
package/dist/instance/setup.js
CHANGED
|
@@ -51,6 +51,6 @@ export async function setup(root, io, options) {
|
|
|
51
51
|
io.say("Everything answers. Last: the rules your assistant follows.");
|
|
52
52
|
if (options.installSkills)
|
|
53
53
|
await options.installSkills();
|
|
54
|
-
io.say("Done. Try `penpair pages`, or tell your assistant to read the
|
|
54
|
+
io.say("Done. Try `penpair pages`, or tell your assistant to read the penpair-design skill and start.");
|
|
55
55
|
return { config, connect };
|
|
56
56
|
}
|
package/dist/skills.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface InstallOptions {
|
|
|
20
20
|
/** For tests. */
|
|
21
21
|
source?: string;
|
|
22
22
|
}
|
|
23
|
+
/** Skills that changed name. Installing the new one leaves the old folder behind, which `installSkills` then reports. */
|
|
24
|
+
export declare const renamedSkills: Record<string, string>;
|
|
23
25
|
export interface InstallReport {
|
|
24
26
|
installed: string[];
|
|
25
27
|
unchanged: string[];
|
|
@@ -28,5 +30,11 @@ export interface InstallReport {
|
|
|
28
30
|
linked: string[];
|
|
29
31
|
/** One line per skill for the project's AGENTS.md. */
|
|
30
32
|
agentsRows: string[];
|
|
33
|
+
/** Folders of skills that were renamed and are still in the project, and whether their project-facts file was carried over. */
|
|
34
|
+
legacy: {
|
|
35
|
+
folder: string;
|
|
36
|
+
renamedTo: string;
|
|
37
|
+
carriedFacts: boolean;
|
|
38
|
+
}[];
|
|
31
39
|
}
|
|
32
40
|
export declare function installSkills(options: InstallOptions): Promise<InstallReport>;
|
package/dist/skills.js
CHANGED
|
@@ -44,6 +44,8 @@ export async function describeSkill(skillDirectory) {
|
|
|
44
44
|
const description = /^description:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "";
|
|
45
45
|
return { name, summary: description.split(/(?<=\.)\s/)[0] ?? description };
|
|
46
46
|
}
|
|
47
|
+
/** Skills that changed name. Installing the new one leaves the old folder behind, which `installSkills` then reports. */
|
|
48
|
+
export const renamedSkills = { "penpot-design": "penpair-design" };
|
|
47
49
|
export async function installSkills(options) {
|
|
48
50
|
const source = options.source ?? bundledSkillsDirectory();
|
|
49
51
|
const to = options.to ?? ".skills";
|
|
@@ -52,7 +54,7 @@ export async function installSkills(options) {
|
|
|
52
54
|
for (const name of chosen)
|
|
53
55
|
if (!names.includes(name))
|
|
54
56
|
throw new Error(`no skill named ${name}; shipped: ${names.join(", ")}`);
|
|
55
|
-
const report = { installed: [], unchanged: [], kept: [], linked: [], agentsRows: [] };
|
|
57
|
+
const report = { installed: [], unchanged: [], kept: [], linked: [], agentsRows: [], legacy: [] };
|
|
56
58
|
for (const name of chosen) {
|
|
57
59
|
const from = path.join(source, name);
|
|
58
60
|
for (const file of await filesUnder(from)) {
|
|
@@ -87,5 +89,18 @@ export async function installSkills(options) {
|
|
|
87
89
|
const { summary } = await describeSkill(from);
|
|
88
90
|
report.agentsRows.push(`| [${name}](${path.posix.join(to, name, "SKILL.md")}) | ${summary} |`);
|
|
89
91
|
}
|
|
92
|
+
for (const [oldName, newName] of Object.entries(renamedSkills)) {
|
|
93
|
+
if (!chosen.includes(newName) || !(await exists(path.join(options.root, to, oldName))))
|
|
94
|
+
continue;
|
|
95
|
+
// The project-facts file is the project's own work: carry it over when the new copy is still the empty template.
|
|
96
|
+
const oldFacts = await readIfExists(path.join(options.root, to, oldName, "references/project-facts.md"));
|
|
97
|
+
const newFactsFile = path.join(options.root, to, newName, "references/project-facts.md");
|
|
98
|
+
const template = await readIfExists(path.join(source, newName, "references/project-facts.md"));
|
|
99
|
+
const current = await readIfExists(newFactsFile);
|
|
100
|
+
const carriedFacts = oldFacts !== undefined && oldFacts !== template && current === template;
|
|
101
|
+
if (carriedFacts)
|
|
102
|
+
await writeFile(newFactsFile, oldFacts);
|
|
103
|
+
report.legacy.push({ folder: path.posix.join(to, oldName), renamedTo: newName, carriedFacts });
|
|
104
|
+
}
|
|
90
105
|
return report;
|
|
91
106
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
|
|
2
|
-
export declare const version = "0.1.
|
|
2
|
+
export declare const version = "0.1.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Kept in its own file so that the command line and the library entry can both read it without importing each other. Equals package.json (a test checks). */
|
|
2
|
-
export const version = "0.1.
|
|
2
|
+
export const version = "0.1.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penpair/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Design together. Build together. The library and engine behind penpair: connect AI agents and people to a Penpot design file, safely. Install @penpair/web or @penpair/docker for the command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
25
|
},
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: design-judgment
|
|
3
|
-
description: Decide how a screen is structured, filled and judged before it is built. Use when designing or redesigning a page or flow (list, detail, queue, dashboard, admin), choosing a composition or density, deciding what fills a card or panel, defining loading, empty and error states, mapping status or feedback to components, comparing layout options, or critiquing a design or a rendered screen. Do not use for writing to Penpot (
|
|
3
|
+
description: Decide how a screen is structured, filled and judged before it is built. Use when designing or redesigning a page or flow (list, detail, queue, dashboard, admin), choosing a composition or density, deciding what fills a card or panel, defining loading, empty and error states, mapping status or feedback to components, comparing layout options, or critiquing a design or a rendered screen. Do not use for writing to Penpot (penpair-design) or for picking and verifying components of a specific library.
|
|
4
4
|
metadata:
|
|
5
5
|
version: "0.1.0"
|
|
6
6
|
scope: design reasoning
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: penpair-design
|
|
3
3
|
description: Work in a Penpot file through penpair: inspect, build, change, audit, export and archive frames and components, bind every value to a token, keep layout as flat auto-layout, and run Penpot as one instance per project. Use when creating, editing, inspecting or exporting anything in Penpot, adding or binding tokens, writing a project snippet, setting up or checking a Penpot instance, or diagnosing a plugin or connection error. Do not use for deciding how a screen should be structured (design-judgment).
|
|
4
4
|
metadata:
|
|
5
5
|
version: "0.1.0"
|
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
compatibility: A running Penpot (local Docker, a team server or the hosted service) with the plugin connected in a visible editor tab, Node 22, penpair installed.
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# penpair-design
|
|
12
12
|
|
|
13
13
|
Penpot is the authority for the design; the code adapts to the frames. The project's component library defines what exists: a component, token or layout the library has wins, and only what it lacks becomes a project-specific component or token.
|
|
14
14
|
|
|
@@ -43,6 +43,8 @@ Each one cost real time. They are written as the behaviour to use. Penpot 2.18.3
|
|
|
43
43
|
- **Exports can fail once and then work, and can be stale right after a write or a page switch.** Export again, and compare two in a row.
|
|
44
44
|
- The MCP key is personal, one per account, shown once. A key for another instance silently drives another file; penpair refuses it unless told otherwise.
|
|
45
45
|
- A snippet receives data only through its parameters; it cannot read repository files.
|
|
46
|
+
- **Token values can be changed in place** (`token.value = ...`, `token.description = ...`), and `token.remove()`, `set.remove()`, `theme.addSet(set)`, `theme.removeSet(set)` and `theme.remove()` all work. **Not for a typography token:** creating one normalises it (`fontFamilies: "Inter"` becomes `fontFamily: ["Inter"]`), but assigning a new value stores the object as given, un-normalised. Remove the token and add it again, and compare typography values in their normalised form. (Checked on Penpot 2.18.4, hosted.)
|
|
47
|
+
- A new token set is inactive and a set's `resolvedValue` is null until a theme activates it; a file with no active theme resolves no tokens at all.
|
|
46
48
|
|
|
47
49
|
## Rendering differences to design for
|
|
48
50
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: penpair-setup
|
|
3
|
-
description: Set up penpair in a project and connect it to a Penpot, on the web (the default) or in Docker on this computer. Use when a project has no penpair.config.json, when `penpair doctor` reports a failure, when the user asks to install, start, connect, repair or move their Penpot connection, or when an agent is told "set up penpair". Do not use for editing a Penpot file (
|
|
3
|
+
description: Set up penpair in a project and connect it to a Penpot, on the web (the default) or in Docker on this computer. Use when a project has no penpair.config.json, when `penpair doctor` reports a failure, when the user asks to install, start, connect, repair or move their Penpot connection, or when an agent is told "set up penpair". Do not use for editing a Penpot file (penpair-design) or deciding how a screen looks (design-judgment).
|
|
4
4
|
metadata:
|
|
5
5
|
version: "0.1.0"
|
|
6
6
|
scope: setup and connection
|
|
@@ -39,7 +39,7 @@ The MCP key is the person's password to their Penpot. Keep it out of chat, logs,
|
|
|
39
39
|
| editor and plugin: not connected or suspended | The tab is asleep or the plugin is not connected. Do step 4.4 again; keep the tab in front. |
|
|
40
40
|
| key file: git would commit it | Add `.env.local` to `.gitignore`; `penpair connect` does it. |
|
|
41
41
|
|
|
42
|
-
6. **Install the rules.** Run `npx @penpair/web skills install`, add the printed rows to the project's agent guide, then fill in `.skills/
|
|
42
|
+
6. **Install the rules.** Run `npx @penpair/web skills install`, add the printed rows to the project's agent guide, then fill in `.skills/penpair-design/references/project-facts.md` with the person (the Penpot file, token sets, naming, who approves). Done when that file has no `<...>` placeholders left.
|
|
43
43
|
|
|
44
44
|
## Moving between web and Docker
|
|
45
45
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|