@penpair/marwes-ui 0.1.3 → 0.3.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/dist/build-kit.d.ts +3 -0
- package/dist/build-kit.js +93 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +633 -92
- package/dist/components.d.ts +95 -0
- package/dist/components.js +497 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +80 -7
- package/dist/index.d.ts +13 -0
- package/dist/index.js +30 -1
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.d.ts +95 -0
- package/dist/mode-trial.js +479 -0
- package/dist/ownership.d.ts +26 -0
- package/dist/ownership.js +222 -0
- package/dist/penpot.d.ts +21 -3
- package/dist/penpot.js +519 -87
- package/dist/reconciliation.d.ts +6 -0
- package/dist/reconciliation.js +179 -0
- package/dist/resolved-values.d.ts +11 -0
- package/dist/resolved-values.js +124 -0
- package/dist/snapshot-file.d.ts +45 -0
- package/dist/snapshot-file.js +282 -40
- package/dist/theme-input.js +18 -7
- package/dist/theme-lint.d.ts +31 -0
- package/dist/theme-lint.js +145 -0
- package/dist/tokens.d.ts +6 -2
- package/dist/tokens.js +221 -45
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -1,35 +1,104 @@
|
|
|
1
|
+
import { lintThemeFamily } from "./theme-lint.js";
|
|
2
|
+
import { componentsCommand } from "./components.js";
|
|
3
|
+
import { runModeTrial } from "./mode-trial.js";
|
|
4
|
+
import { ownershipReport, readSyncAncestor, saveSyncAncestor } from "./ownership.js";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
1
6
|
import { createRequire } from "node:module";
|
|
2
7
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { loadConfig, readRawConfig } from "@penpair/core";
|
|
8
|
+
import { projectTheme, parseCommandArgs, readDesignJson, atomicWriteFile, resolveProjectOutput, readJournalTarget, guardFile, resolveProjectInput, createCheckpoint, loadConfig, readRawConfig, } from "@penpair/core";
|
|
5
9
|
import { readMarwesSettings } from "./config.js";
|
|
6
10
|
import { familyProblems, loadInstalledMarwes } from "./marwes.js";
|
|
7
|
-
import { applyPlan, compareToPenpot, describePlan, formatDrift, impactOf, planPush, readLive } from "./penpot.js";
|
|
8
|
-
import { diffSnapshots, formatDiff, readSnapshot, writeSnapshot } from "./snapshot-file.js";
|
|
11
|
+
import { applyPlan, compareToPenpot, describePlan, formatDrift, impactOf, planPush, readLive, } from "./penpot.js";
|
|
12
|
+
import { diffSnapshots, formatDiff, tokenSnapshotSchema, migrateTokenSnapshot, readSnapshot, writeSnapshot, } from "./snapshot-file.js";
|
|
9
13
|
import { loadThemeInput } from "./theme-input.js";
|
|
10
14
|
import { buildSnapshot } from "./tokens.js";
|
|
15
|
+
const print = (context, ...values) => (context.output?.log ?? console.log)(...values);
|
|
16
|
+
const diagnostic = (context, ...values) => (context.output?.error ?? console.error)(...values);
|
|
11
17
|
const adapterVersion = createRequire(import.meta.url)("../package.json").version;
|
|
12
18
|
export async function computeExpectation(root, rawConfig, instance) {
|
|
13
19
|
const settings = readMarwesSettings(rawConfig, instance);
|
|
14
|
-
const installed = await loadInstalledMarwes(
|
|
15
|
-
const [light, dark] = await Promise.all([
|
|
20
|
+
const installed = await loadInstalledMarwes(await resolveProjectInput(root, settings.project));
|
|
21
|
+
const [light, dark] = await Promise.all([
|
|
22
|
+
settings.themes.light,
|
|
23
|
+
settings.themes.dark,
|
|
24
|
+
].map((reference) => (reference ? loadThemeInput(root, reference) : undefined)));
|
|
16
25
|
const fresh = buildSnapshot({
|
|
17
26
|
core: installed.core,
|
|
18
27
|
version: installed.version,
|
|
19
28
|
name: settings.name,
|
|
20
|
-
|
|
29
|
+
extras: settings.extras,
|
|
30
|
+
project: {
|
|
31
|
+
...(light
|
|
32
|
+
? {
|
|
33
|
+
light: light.input,
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
...(dark
|
|
37
|
+
? {
|
|
38
|
+
dark: dark.input,
|
|
39
|
+
}
|
|
40
|
+
: {}),
|
|
41
|
+
},
|
|
21
42
|
generatedBy: `@penpair/marwes-ui@${adapterVersion}`,
|
|
22
43
|
});
|
|
23
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
settings,
|
|
46
|
+
installed,
|
|
47
|
+
fresh,
|
|
48
|
+
problems: familyProblems(installed),
|
|
49
|
+
};
|
|
24
50
|
}
|
|
25
51
|
export async function checkSnapshotFor(root, rawConfig, instance) {
|
|
26
52
|
const expectation = await computeExpectation(root, rawConfig, instance);
|
|
27
53
|
const committed = await readSnapshot(root, expectation.settings.snapshot);
|
|
28
54
|
const diff = diffSnapshots(committed, expectation.fresh);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
const reasons = expectation.problems.map((message) => ({
|
|
56
|
+
code: "package-mismatch",
|
|
57
|
+
message,
|
|
58
|
+
}));
|
|
59
|
+
const migration = `Preview migration with \`penpair marwes tokens migrate --from ${expectation.settings.snapshot} --ownership ownership.json\`; explicitly classify every set as adapter or project, then use --out NEW_FILE to preserve the original.`;
|
|
60
|
+
if (!committed) {
|
|
61
|
+
reasons.push({
|
|
62
|
+
code: "missing-snapshot",
|
|
63
|
+
message: `there is no snapshot yet at ${expectation.settings.snapshot}; run \`penpair marwes tokens snapshot --write\` and commit it`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (committed.schemaVersion !== expectation.fresh.schemaVersion) {
|
|
68
|
+
reasons.push({
|
|
69
|
+
code: "schema-migration",
|
|
70
|
+
message: `Snapshot schema ${committed.schemaVersion} needs migration to ${expectation.fresh.schemaVersion}. ${migration}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (!committed.ownership) {
|
|
74
|
+
reasons.push({
|
|
75
|
+
code: "missing-ownership",
|
|
76
|
+
message: `Legacy snapshot has no set ownership. ${migration}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!diff.identical) {
|
|
80
|
+
reasons.push({
|
|
81
|
+
code: "content-drift",
|
|
82
|
+
message: "Snapshot values, sets, themes or installed package version changed; review the value diff before regenerating.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (committed.hash !== expectation.fresh.hash) {
|
|
86
|
+
reasons.push({
|
|
87
|
+
code: "metadata-mismatch",
|
|
88
|
+
message: diff.identical
|
|
89
|
+
? "Token values are identical, but snapshot metadata/hash differs; review schema, ownership and provenance before replacing it."
|
|
90
|
+
: "Snapshot hash differs from the current expectation.",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
current: reasons.length === 0,
|
|
96
|
+
committed,
|
|
97
|
+
diff,
|
|
98
|
+
reasons,
|
|
99
|
+
problems: reasons.map((reason) => reason.message),
|
|
100
|
+
expectation,
|
|
101
|
+
};
|
|
33
102
|
}
|
|
34
103
|
/**
|
|
35
104
|
* For a project's own test suite: resolves the project's penpair.config.json and checks that the committed Marwes token snapshot
|
|
@@ -41,135 +110,426 @@ export async function checkTokenSnapshot(root) {
|
|
|
41
110
|
}
|
|
42
111
|
const summary = (snapshot) => `Marwes ${snapshot.marwesVersion}, ${snapshot.sets.length} sets, ${snapshot.sets.reduce((n, s) => n + s.tokens.length, 0)} tokens, ${snapshot.hash.slice(0, 10)}`;
|
|
43
112
|
/** What the snapshot says it does not cover, so a reader is never left thinking everything Marwes provides is in Penpot. */
|
|
44
|
-
const coverage = (snapshot) => (
|
|
45
|
-
|
|
113
|
+
const coverage = (snapshot) => (snapshot.unmapped ?? []).length > 0
|
|
114
|
+
? `${snapshot.unmapped.length} Marwes variable(s) are not tokens: ${snapshot.unmapped.slice(0, 6).join(", ")}${snapshot.unmapped.length > 6 ? ", ..." : ""}`
|
|
115
|
+
: undefined;
|
|
116
|
+
const flags = (args) => parseCommandArgs({
|
|
117
|
+
args,
|
|
118
|
+
allowPositionals: true,
|
|
119
|
+
options: {
|
|
120
|
+
from: {
|
|
121
|
+
type: "string",
|
|
122
|
+
},
|
|
123
|
+
ownership: {
|
|
124
|
+
type: "string",
|
|
125
|
+
},
|
|
126
|
+
theme: {
|
|
127
|
+
type: "string",
|
|
128
|
+
},
|
|
129
|
+
resolved: {
|
|
130
|
+
type: "boolean",
|
|
131
|
+
},
|
|
132
|
+
page: {
|
|
133
|
+
type: "string",
|
|
134
|
+
},
|
|
135
|
+
out: {
|
|
136
|
+
type: "string",
|
|
137
|
+
},
|
|
138
|
+
overwrite: {
|
|
139
|
+
type: "boolean",
|
|
140
|
+
},
|
|
141
|
+
adopt: {
|
|
142
|
+
type: "string",
|
|
143
|
+
},
|
|
144
|
+
write: {
|
|
145
|
+
type: "boolean",
|
|
146
|
+
},
|
|
147
|
+
apply: {
|
|
148
|
+
type: "boolean",
|
|
149
|
+
},
|
|
150
|
+
prune: {
|
|
151
|
+
type: "boolean",
|
|
152
|
+
},
|
|
153
|
+
json: {
|
|
154
|
+
type: "boolean",
|
|
155
|
+
},
|
|
156
|
+
resume: {
|
|
157
|
+
type: "string",
|
|
158
|
+
},
|
|
159
|
+
checkpoint: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
},
|
|
162
|
+
"force-lock": {
|
|
163
|
+
type: "boolean",
|
|
164
|
+
},
|
|
165
|
+
trace: {
|
|
166
|
+
type: "boolean",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
46
170
|
const staleAdvice = "Run `penpair marwes tokens snapshot --write`, review the change in git, and commit it.";
|
|
47
171
|
async function snapshotCommand(context, args) {
|
|
48
172
|
const { values } = flags(args);
|
|
49
173
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
50
174
|
const fresh = check.expectation.fresh;
|
|
51
|
-
if (values.json)
|
|
52
|
-
|
|
175
|
+
if (values.json) {
|
|
176
|
+
print(context, JSON.stringify({
|
|
177
|
+
current: check.current,
|
|
178
|
+
summary: summary(fresh),
|
|
179
|
+
diff: check.diff,
|
|
180
|
+
reasons: check.reasons,
|
|
181
|
+
problems: check.problems,
|
|
182
|
+
}, null, 2));
|
|
183
|
+
}
|
|
53
184
|
else {
|
|
54
|
-
|
|
55
|
-
for (const line of formatDiff(check.diff))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
185
|
+
print(context, `Expected from the installed ${summary(fresh)}`);
|
|
186
|
+
for (const line of formatDiff(check.diff)) {
|
|
187
|
+
print(context, ` ${line}`);
|
|
188
|
+
}
|
|
189
|
+
if (coverage(fresh)) {
|
|
190
|
+
print(context, `note ${coverage(fresh)}`);
|
|
191
|
+
}
|
|
192
|
+
for (const problem of check.problems.filter((p) => !p.startsWith("there is no snapshot"))) {
|
|
193
|
+
print(context, `FAIL ${problem}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (check.expectation.problems.length > 0) {
|
|
63
197
|
return 1;
|
|
198
|
+
}
|
|
64
199
|
if (check.current) {
|
|
65
|
-
if (!values.json)
|
|
66
|
-
|
|
200
|
+
if (!values.json) {
|
|
201
|
+
print(context, `ok ${check.expectation.settings.snapshot} is current`);
|
|
202
|
+
}
|
|
67
203
|
return 0;
|
|
68
204
|
}
|
|
69
205
|
if (!values.write) {
|
|
70
|
-
if (!values.json)
|
|
71
|
-
|
|
206
|
+
if (!values.json) {
|
|
207
|
+
print(context, `${check.committed ? "The committed snapshot is stale." : "There is no snapshot yet."} Run again with --write to save ${check.expectation.settings.snapshot}.`);
|
|
208
|
+
}
|
|
72
209
|
return 2;
|
|
73
210
|
}
|
|
74
211
|
await writeSnapshot(context.root, check.expectation.settings.snapshot, fresh);
|
|
75
|
-
|
|
76
|
-
|
|
212
|
+
context.output?.result({
|
|
213
|
+
current: true,
|
|
214
|
+
snapshot: check.expectation.settings.snapshot,
|
|
215
|
+
hash: fresh.hash,
|
|
216
|
+
written: true,
|
|
217
|
+
});
|
|
218
|
+
if (!values.json) {
|
|
219
|
+
print(context, `wrote ${check.expectation.settings.snapshot}. Review the change in git and commit it; then \`penpair marwes tokens diff\` shows what Penpot needs.`);
|
|
220
|
+
}
|
|
77
221
|
return 0;
|
|
78
222
|
}
|
|
79
223
|
async function checkCommand(context, args) {
|
|
80
224
|
const { values } = flags(args);
|
|
81
225
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
82
226
|
if (values.json) {
|
|
83
|
-
|
|
227
|
+
print(context, JSON.stringify({
|
|
228
|
+
current: check.current,
|
|
229
|
+
installed: check.expectation.installed.versions,
|
|
230
|
+
diff: check.diff,
|
|
231
|
+
reasons: check.reasons,
|
|
232
|
+
problems: check.problems,
|
|
233
|
+
}, null, 2));
|
|
84
234
|
return check.current ? 0 : check.expectation.problems.length > 0 ? 1 : 2;
|
|
85
235
|
}
|
|
86
236
|
if (check.current) {
|
|
87
|
-
|
|
237
|
+
print(context, `ok ${check.expectation.settings.snapshot} is current (${summary(check.expectation.fresh)})`);
|
|
88
238
|
return 0;
|
|
89
239
|
}
|
|
90
|
-
for (const line of formatDiff(check.diff))
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
240
|
+
for (const line of formatDiff(check.diff)) {
|
|
241
|
+
print(context, ` ${line}`);
|
|
242
|
+
}
|
|
243
|
+
for (const problem of check.problems) {
|
|
244
|
+
print(context, `FAIL ${problem}`);
|
|
245
|
+
}
|
|
246
|
+
if (check.committed &&
|
|
247
|
+
check.expectation.problems.length === 0 &&
|
|
248
|
+
!check.reasons.some((reason) => reason.code === "schema-migration")) {
|
|
249
|
+
const cause = check.committed.marwesVersion !== check.expectation.installed.version
|
|
250
|
+
? `it was made from Marwes ${check.committed.marwesVersion} and the installed Marwes is ${check.expectation.installed.version}`
|
|
251
|
+
: "the installed Marwes or the project's theme has changed since it was made";
|
|
252
|
+
print(context, `FAIL the committed snapshot is stale: ${cause}. ${staleAdvice}`);
|
|
97
253
|
}
|
|
98
254
|
return check.expectation.problems.length > 0 ? 1 : 2;
|
|
99
255
|
}
|
|
100
256
|
async function readPenpot(context, committed, wantImpact) {
|
|
101
257
|
return context.withPenpot(async (client) => {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
258
|
+
const target = await readJournalTarget(client);
|
|
259
|
+
const pinned = guardFile(client, {
|
|
260
|
+
id: target.fileId,
|
|
261
|
+
});
|
|
262
|
+
const live = await readLive(pinned);
|
|
263
|
+
const after = await readJournalTarget(pinned);
|
|
264
|
+
if (after.fileId !== target.fileId || after.revision !== target.revision) {
|
|
265
|
+
throw new Error("File changed during token inspection; inspect again");
|
|
266
|
+
}
|
|
267
|
+
const ancestor = await readSyncAncestor(context.root, target.fileId);
|
|
268
|
+
const drift = compareToPenpot(committed, live, {
|
|
269
|
+
ancestor,
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
target,
|
|
273
|
+
live,
|
|
274
|
+
drift,
|
|
275
|
+
...(wantImpact && !drift.inSync
|
|
276
|
+
? {
|
|
277
|
+
impact: await impactOf(pinned).catch(() => undefined),
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
105
281
|
});
|
|
106
282
|
}
|
|
107
283
|
async function diffCommand(context, args) {
|
|
108
284
|
const { values } = flags(args);
|
|
285
|
+
if (values.resolved) {
|
|
286
|
+
return modeCommand(context, args);
|
|
287
|
+
}
|
|
109
288
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
110
289
|
if (!check.committed) {
|
|
111
|
-
|
|
290
|
+
diagnostic(context, `FAIL ${check.problems.join("; ")}`);
|
|
112
291
|
return 1;
|
|
113
292
|
}
|
|
114
293
|
const { drift, impact } = await readPenpot(context, check.committed, true);
|
|
115
294
|
if (values.json) {
|
|
116
|
-
|
|
295
|
+
print(context, JSON.stringify({
|
|
296
|
+
snapshotCurrent: check.current,
|
|
297
|
+
drift,
|
|
298
|
+
impact,
|
|
299
|
+
}, null, 2));
|
|
117
300
|
return drift.inSync ? 0 : 2;
|
|
118
301
|
}
|
|
119
|
-
if (!check.current)
|
|
120
|
-
|
|
302
|
+
if (!check.current) {
|
|
303
|
+
print(context, `note: the committed snapshot is stale against the installed Marwes (${staleAdvice}); the comparison below is against the committed one.`);
|
|
304
|
+
}
|
|
121
305
|
if (drift.inSync) {
|
|
122
|
-
|
|
306
|
+
for (const line of formatDrift(drift, impact)) {
|
|
307
|
+
print(context, `note preserved: ${line}`);
|
|
308
|
+
}
|
|
309
|
+
print(context, `ok Penpot matches ${summary(check.committed)}`);
|
|
123
310
|
return 0;
|
|
124
311
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
312
|
+
print(context, values.resume
|
|
313
|
+
? `Resuming run ${values.resume}; current snapshot: ${summary(check.committed)}`
|
|
314
|
+
: `Penpot differs from ${summary(check.committed)}:`);
|
|
315
|
+
for (const line of formatDrift(drift, impact)) {
|
|
316
|
+
print(context, ` ${line}`);
|
|
317
|
+
}
|
|
318
|
+
print(context, "`penpair marwes tokens push --apply` brings Penpot to the snapshot (designer edits require explicit adoption or overwrite).");
|
|
129
319
|
return 2;
|
|
130
320
|
}
|
|
321
|
+
async function modeCommand(context, args, frameId) {
|
|
322
|
+
const { values } = flags(args);
|
|
323
|
+
if ((values.resume || values.checkpoint || values["force-lock"] || values.trace) &&
|
|
324
|
+
!values.apply) {
|
|
325
|
+
throw new Error("Mode resume/checkpoint/force-lock require --apply");
|
|
326
|
+
}
|
|
327
|
+
if (frameId && (!values.page || !values.out)) {
|
|
328
|
+
throw new Error("verify-modes requires one frame ID, --page and --out");
|
|
329
|
+
}
|
|
330
|
+
if (values.out) {
|
|
331
|
+
await resolveProjectOutput(context.root, values.out);
|
|
332
|
+
}
|
|
333
|
+
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
334
|
+
if (!check.committed || !check.current) {
|
|
335
|
+
diagnostic(context, "Mode verification requires a current committed snapshot. " + staleAdvice);
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
const report = await context.withPenpot((client) => runModeTrial(client, check.committed, {
|
|
339
|
+
root: context.root,
|
|
340
|
+
apply: values.apply === true,
|
|
341
|
+
resume: values.resume,
|
|
342
|
+
forceLock: values["force-lock"] === true,
|
|
343
|
+
redact: context.redact,
|
|
344
|
+
onStep: context.log,
|
|
345
|
+
requireCheckpoint: values.checkpoint === true,
|
|
346
|
+
...(values.checkpoint
|
|
347
|
+
? {
|
|
348
|
+
checkpoint: (runId) => createCheckpoint(client, {
|
|
349
|
+
label: "mode trial",
|
|
350
|
+
runId,
|
|
351
|
+
}, {
|
|
352
|
+
apply: true,
|
|
353
|
+
}),
|
|
354
|
+
}
|
|
355
|
+
: {}),
|
|
356
|
+
...(frameId
|
|
357
|
+
? {
|
|
358
|
+
frame: {
|
|
359
|
+
id: frameId,
|
|
360
|
+
page: values.page,
|
|
361
|
+
out: values.out,
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
}));
|
|
366
|
+
context.output?.result(report);
|
|
367
|
+
print(context, JSON.stringify(report, null, 2));
|
|
368
|
+
if (!values.apply) {
|
|
369
|
+
diagnostic(context, "Mode checks temporarily change active selection. Inspect planned states, then use --apply; the full original selection will be restored and verified separately.");
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
return report.uncertain
|
|
373
|
+
? 4
|
|
374
|
+
: !report.restoration.verified
|
|
375
|
+
? 1
|
|
376
|
+
: !report.complete
|
|
377
|
+
? 3
|
|
378
|
+
: report.verified
|
|
379
|
+
? 0
|
|
380
|
+
: 2;
|
|
381
|
+
}
|
|
131
382
|
async function pushCommand(context, args) {
|
|
132
383
|
const { values } = flags(args);
|
|
384
|
+
if ((values.resume || values.checkpoint || values["force-lock"] || values.trace) &&
|
|
385
|
+
!values.apply) {
|
|
386
|
+
throw new Error("Resume/checkpoint require --apply; inspect the journal and current design first");
|
|
387
|
+
}
|
|
133
388
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
134
389
|
if (!check.committed) {
|
|
135
|
-
|
|
390
|
+
diagnostic(context, `FAIL ${check.problems.join("; ")}`);
|
|
136
391
|
return 1;
|
|
137
392
|
}
|
|
138
393
|
if (!check.current) {
|
|
139
|
-
for (const line of formatDiff(check.diff))
|
|
140
|
-
|
|
141
|
-
|
|
394
|
+
for (const line of formatDiff(check.diff)) {
|
|
395
|
+
print(context, ` ${line}`);
|
|
396
|
+
}
|
|
397
|
+
diagnostic(context, `STOP the committed snapshot is stale against the installed Marwes or the theme, and the repository's record must lead. ${staleAdvice} Then push.`);
|
|
142
398
|
return 2;
|
|
143
399
|
}
|
|
144
|
-
const { drift, impact, live } = await readPenpot(context, check.committed, true);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
400
|
+
const { drift, impact, live, target } = await readPenpot(context, check.committed, true);
|
|
401
|
+
if (values.adopt && (values.apply || values.overwrite || values.resume)) {
|
|
402
|
+
throw new Error("--adopt writes a local review proposal and cannot be combined with apply/overwrite/resume");
|
|
403
|
+
}
|
|
404
|
+
const ancestor = await readSyncAncestor(context.root, target.fileId);
|
|
405
|
+
const ownership = ownershipReport(check.committed, live, ancestor);
|
|
406
|
+
if (values.adopt) {
|
|
407
|
+
const proposal = JSON.stringify({
|
|
408
|
+
schemaVersion: 1,
|
|
409
|
+
kind: "token-adoption-proposal",
|
|
410
|
+
fileId: target.fileId,
|
|
411
|
+
snapshotHash: check.committed.hash,
|
|
412
|
+
ownership,
|
|
413
|
+
live,
|
|
414
|
+
instructions: "Review live values and declare intentional changes in project themes/extras; regenerate the snapshot. This proposal changes no design or ownership ancestor.",
|
|
415
|
+
}, null, 2) + "\n";
|
|
416
|
+
if (context.redact && context.redact(proposal) !== proposal) {
|
|
417
|
+
throw new Error("Adoption evidence requires credential redaction; no proposal was published");
|
|
418
|
+
}
|
|
419
|
+
const out = await resolveProjectOutput(context.root, values.adopt);
|
|
420
|
+
await mkdir(path.dirname(out), {
|
|
421
|
+
recursive: true,
|
|
422
|
+
});
|
|
423
|
+
await atomicWriteFile(out, proposal);
|
|
424
|
+
context.output?.result({
|
|
425
|
+
phase: "adoption-proposed",
|
|
426
|
+
file: values.adopt,
|
|
427
|
+
ownership,
|
|
428
|
+
complete: false,
|
|
429
|
+
});
|
|
430
|
+
print(context, "Saved adoption proposal. Review declarations and regenerate the snapshot before pushing; no Penpot write or ancestor update occurred.");
|
|
431
|
+
return 3;
|
|
432
|
+
}
|
|
433
|
+
const plan = planPush(check.committed, live, {
|
|
434
|
+
prune: values.prune === true,
|
|
435
|
+
ancestor,
|
|
436
|
+
});
|
|
437
|
+
if (ownership.blockers.length && !values.overwrite && !values.resume) {
|
|
438
|
+
context.output?.result({
|
|
439
|
+
phase: "conflict",
|
|
440
|
+
ownership,
|
|
441
|
+
plan,
|
|
442
|
+
verified: false,
|
|
443
|
+
});
|
|
444
|
+
diagnostic(context, `STOP ${ownership.blockers.length} designer/conflicting/untracked token properties need a decision. Use --adopt <new-file.json> for a local review proposal, or inspect --overwrite and explicitly apply it.`);
|
|
445
|
+
return 2;
|
|
446
|
+
}
|
|
447
|
+
if (drift.inSync && plan.length === 0 && !values.resume) {
|
|
448
|
+
const evidence = JSON.stringify(check.committed);
|
|
449
|
+
if (context.redact && context.redact(evidence) !== evidence) {
|
|
450
|
+
throw new Error("Sync evidence requires credential redaction");
|
|
451
|
+
}
|
|
452
|
+
if (values.apply) {
|
|
453
|
+
await saveSyncAncestor(context.root, target.fileId, check.committed, ancestor);
|
|
454
|
+
}
|
|
455
|
+
context.output?.result({
|
|
456
|
+
verified: true,
|
|
457
|
+
unchanged: true,
|
|
458
|
+
drift,
|
|
459
|
+
ownership,
|
|
460
|
+
});
|
|
461
|
+
print(context, `ok Penpot already matches ${summary(check.committed)}; nothing to write`);
|
|
148
462
|
return 0;
|
|
149
463
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
464
|
+
print(context, values.resume
|
|
465
|
+
? `Resuming run ${values.resume}; current snapshot: ${summary(check.committed)}`
|
|
466
|
+
: `Penpot differs from ${summary(check.committed)}:`);
|
|
467
|
+
for (const line of formatDrift(drift, impact)) {
|
|
468
|
+
print(context, ` ${line}`);
|
|
469
|
+
}
|
|
470
|
+
print(context, "Plan:");
|
|
471
|
+
for (const line of describePlan(plan)) {
|
|
472
|
+
print(context, ` ${line}`);
|
|
473
|
+
}
|
|
156
474
|
if (!values.apply) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
475
|
+
context.output?.result({
|
|
476
|
+
phase: "inspected",
|
|
477
|
+
ownership,
|
|
478
|
+
plan,
|
|
479
|
+
drift,
|
|
480
|
+
snapshotCurrent: check.current,
|
|
481
|
+
verified: false,
|
|
482
|
+
});
|
|
483
|
+
print(context, "Nothing was written. Run again with --apply to do it.");
|
|
484
|
+
if (!values.prune && drift.sets.some((s) => s.tokens.some((t) => t.kind === "extra"))) {
|
|
485
|
+
print(context, "Tokens without a verified ownership ancestor are preserved, including with --prune.");
|
|
486
|
+
}
|
|
160
487
|
return 0;
|
|
161
488
|
}
|
|
162
|
-
const report = await context.withPenpot((client) => applyPlan(client, check.committed, plan, (message) => context.log(message)
|
|
163
|
-
|
|
164
|
-
|
|
489
|
+
const report = await context.withPenpot((client) => applyPlan(client, check.committed, plan, (message) => context.log(message), {
|
|
490
|
+
prune: values.prune === true,
|
|
491
|
+
ancestor,
|
|
492
|
+
expectedFileId: target.fileId,
|
|
493
|
+
expectedLive: live,
|
|
494
|
+
journal: {
|
|
495
|
+
root: context.root,
|
|
496
|
+
trace: values.trace === true,
|
|
497
|
+
forceLock: values["force-lock"] === true,
|
|
498
|
+
runId: values.resume,
|
|
499
|
+
resume: Boolean(values.resume),
|
|
500
|
+
redact: context.redact,
|
|
501
|
+
requireCheckpoint: values.checkpoint === true,
|
|
502
|
+
...(values.checkpoint
|
|
503
|
+
? {
|
|
504
|
+
checkpoint: (runId) => createCheckpoint(client, {
|
|
505
|
+
label: "token push",
|
|
506
|
+
runId,
|
|
507
|
+
}, {
|
|
508
|
+
apply: true,
|
|
509
|
+
}),
|
|
510
|
+
}
|
|
511
|
+
: {}),
|
|
512
|
+
},
|
|
513
|
+
}));
|
|
514
|
+
context.output?.result(report);
|
|
515
|
+
for (const step of report.steps) {
|
|
516
|
+
print(context, `done ${step}`);
|
|
517
|
+
}
|
|
165
518
|
if (report.verified) {
|
|
166
|
-
|
|
519
|
+
const evidence = JSON.stringify(check.committed);
|
|
520
|
+
if (context.redact && context.redact(evidence) !== evidence) {
|
|
521
|
+
throw new Error("Sync evidence requires credential redaction");
|
|
522
|
+
}
|
|
523
|
+
await saveSyncAncestor(context.root, target.fileId, check.committed, ancestor);
|
|
524
|
+
print(context, `ok verified: Penpot now matches the snapshot and no shape moved or was rebound (${report.structureAfter.count} shapes unchanged)`);
|
|
167
525
|
return 0;
|
|
168
526
|
}
|
|
169
|
-
if (!report.structureUnchanged)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
527
|
+
if (!report.structureUnchanged) {
|
|
528
|
+
diagnostic(context, `FAIL the shapes changed while pushing tokens (${report.structureBefore.hash} -> ${report.structureAfter.hash}); look at the file before going on`);
|
|
529
|
+
}
|
|
530
|
+
for (const line of formatDrift(report.drift)) {
|
|
531
|
+
diagnostic(context, `FAIL still different: ${line}`);
|
|
532
|
+
}
|
|
173
533
|
return 1;
|
|
174
534
|
}
|
|
175
535
|
async function statusCommand(context, args) {
|
|
@@ -177,56 +537,237 @@ async function statusCommand(context, args) {
|
|
|
177
537
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
178
538
|
const lines = [];
|
|
179
539
|
let code = 0;
|
|
180
|
-
lines.push(`Marwes installed: ${Object.entries(check.expectation.installed.versions)
|
|
540
|
+
lines.push(`Marwes installed: ${Object.entries(check.expectation.installed.versions)
|
|
541
|
+
.map(([n, v]) => `${n} ${v}`)
|
|
542
|
+
.join(", ")}`);
|
|
181
543
|
for (const problem of check.expectation.problems) {
|
|
182
544
|
lines.push(`FAIL ${problem}`);
|
|
183
545
|
code = 1;
|
|
184
546
|
}
|
|
185
|
-
lines.push(check.committed
|
|
186
|
-
|
|
187
|
-
|
|
547
|
+
lines.push(check.committed
|
|
548
|
+
? `Snapshot (${check.expectation.settings.snapshot}): ${summary(check.committed)}`
|
|
549
|
+
: `Snapshot: none yet (${check.expectation.settings.snapshot})`);
|
|
550
|
+
lines.push(check.current
|
|
551
|
+
? "ok the snapshot is current against the installed Marwes and the theme"
|
|
552
|
+
: `FAIL the snapshot is stale or missing. ${staleAdvice}`);
|
|
553
|
+
if (!check.current && code === 0) {
|
|
188
554
|
code = 2;
|
|
555
|
+
}
|
|
189
556
|
let drift;
|
|
190
557
|
if (check.committed) {
|
|
191
558
|
try {
|
|
192
559
|
drift = (await readPenpot(context, check.committed, false)).drift;
|
|
193
|
-
lines.push(drift.inSync
|
|
194
|
-
|
|
560
|
+
lines.push(drift.inSync
|
|
561
|
+
? "ok Penpot matches the snapshot"
|
|
562
|
+
: `FAIL Penpot differs from the snapshot (${drift.sets.reduce((n, s) => n + s.tokens.length, 0)} token(s), ${drift.themes.length} theme(s)); see \`penpair marwes tokens diff\``);
|
|
563
|
+
if (!drift.inSync && code === 0) {
|
|
195
564
|
code = 2;
|
|
565
|
+
}
|
|
196
566
|
}
|
|
197
567
|
catch (error) {
|
|
568
|
+
if (code === 0) {
|
|
569
|
+
code = 3;
|
|
570
|
+
}
|
|
198
571
|
lines.push(`skip Penpot: not checked (${String(error.message).split("\n")[0]})`);
|
|
199
572
|
}
|
|
200
573
|
}
|
|
201
|
-
if (values.json)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
574
|
+
if (values.json) {
|
|
575
|
+
print(context, JSON.stringify({
|
|
576
|
+
installed: check.expectation.installed.versions,
|
|
577
|
+
snapshotCurrent: check.current,
|
|
578
|
+
penpotInSync: drift?.inSync ?? null,
|
|
579
|
+
complete: Boolean(drift),
|
|
580
|
+
exit: code,
|
|
581
|
+
}, null, 2));
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
for (const line of lines) {
|
|
585
|
+
print(context, line);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
206
588
|
return code;
|
|
207
589
|
}
|
|
208
590
|
export const marwesCommand = {
|
|
209
591
|
name: "marwes",
|
|
210
592
|
summary: "Marwes tokens in Penpot, and drift from the installed Marwes",
|
|
211
593
|
usage: [
|
|
594
|
+
"penpair marwes theme lint [--json] advisory family/default hue gaps; no design writes",
|
|
595
|
+
"penpair marwes tokens schema | migrate --from FILE --ownership FILE [--out NEW_FILE] schema and explicit legacy migration",
|
|
212
596
|
"penpair marwes tokens snapshot [--write] build the expected Penpot tokens from the installed Marwes and your theme; --write saves the snapshot",
|
|
597
|
+
"penpair marwes tokens preview --from SNAPSHOT --theme NAME [--out FILE] offline projected values; no native bindings or live selection changes",
|
|
598
|
+
"penpair marwes tokens migrate --from LEGACY --ownership MAP [--out NEW_FILE] explicit offline ownership upgrade",
|
|
213
599
|
"penpair marwes tokens check [--json] offline, for CI: is the committed snapshot current? (exit 2 when stale)",
|
|
214
600
|
"penpair marwes tokens diff [--json] Penpot against the snapshot: missing, changed, added by hand, and what each change reaches",
|
|
215
|
-
"penpair marwes tokens push [--apply] [--prune]
|
|
601
|
+
"penpair marwes tokens push [--apply] [--prune] inspect reconciliation; --apply writes, --overwrite resolves value conflicts, --prune removes owned retired tokens; --adopt FILE proposes local declarations",
|
|
602
|
+
"penpair marwes tokens diff --resolved [--apply] verify actual values in isolated themes and restore the complete original selection",
|
|
603
|
+
"penpair marwes tokens verify-modes ID --page PAGE --out DIR [--apply] capture stable per-mode PNG evidence and restore selection",
|
|
604
|
+
"penpair marwes components snapshot [--write] | check [--measure] | diff — explicit three-component measurement pilot; native generation gated",
|
|
216
605
|
"penpair marwes tokens status one screen: installed Marwes, snapshot, Penpot",
|
|
217
606
|
],
|
|
218
607
|
async run(context, args) {
|
|
219
608
|
const [area, sub, ...rest] = args;
|
|
609
|
+
if (area === "theme" && sub === "lint") {
|
|
610
|
+
if (rest.some((value) => value !== "--json")) {
|
|
611
|
+
throw new Error("theme lint accepts only --json");
|
|
612
|
+
}
|
|
613
|
+
const expected = await computeExpectation(context.root, context.rawConfig, context.config.instance);
|
|
614
|
+
if (expected.problems.length) {
|
|
615
|
+
throw new Error(expected.problems.join("; "));
|
|
616
|
+
}
|
|
617
|
+
const defaults = buildSnapshot({
|
|
618
|
+
core: expected.installed.core,
|
|
619
|
+
version: expected.installed.version,
|
|
620
|
+
name: expected.settings.name,
|
|
621
|
+
generatedBy: `@penpair/marwes-ui@${adapterVersion}`,
|
|
622
|
+
});
|
|
623
|
+
const report = lintThemeFamily(defaults, expected.fresh, expected.settings.themeLint);
|
|
624
|
+
context.output?.result(report);
|
|
625
|
+
print(context, JSON.stringify(report, null, 2));
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
if (area === "components") {
|
|
629
|
+
return componentsCommand(context, args.slice(1));
|
|
630
|
+
}
|
|
220
631
|
if (area !== "tokens" || !sub) {
|
|
221
|
-
|
|
632
|
+
diagnostic(context, `usage:\n ${marwesCommand.usage.join("\n ")}`);
|
|
222
633
|
return 1;
|
|
223
634
|
}
|
|
224
|
-
|
|
635
|
+
if (sub === "schema") {
|
|
636
|
+
if (rest.some((value) => value !== "--json")) {
|
|
637
|
+
throw new Error("tokens schema accepts only --json");
|
|
638
|
+
}
|
|
639
|
+
context.output?.result(tokenSnapshotSchema);
|
|
640
|
+
print(context, JSON.stringify(tokenSnapshotSchema, null, 2));
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
if (sub === "preview") {
|
|
644
|
+
const { values, positionals } = flags(rest);
|
|
645
|
+
if (positionals.length ||
|
|
646
|
+
!values.from ||
|
|
647
|
+
!values.theme ||
|
|
648
|
+
Object.keys(values).some((key) => ![
|
|
649
|
+
"from",
|
|
650
|
+
"theme",
|
|
651
|
+
"out",
|
|
652
|
+
"json",
|
|
653
|
+
].includes(key))) {
|
|
654
|
+
throw new Error("tokens preview --from SNAPSHOT --theme NAME [--out FILE]");
|
|
655
|
+
}
|
|
656
|
+
const snapshot = await readSnapshot(context.root, values.from);
|
|
657
|
+
if (!snapshot) {
|
|
658
|
+
throw new Error("Preview source snapshot does not exist");
|
|
659
|
+
}
|
|
660
|
+
const preview = {
|
|
661
|
+
...projectTheme(snapshot, values.theme),
|
|
662
|
+
sourceSnapshotHash: snapshot.hash,
|
|
663
|
+
};
|
|
664
|
+
if (values.out) {
|
|
665
|
+
const target = await resolveProjectOutput(context.root, values.out);
|
|
666
|
+
if (target === (await resolveProjectInput(context.root, values.from))) {
|
|
667
|
+
throw new Error("Preview cannot overwrite its source snapshot");
|
|
668
|
+
}
|
|
669
|
+
await mkdir(path.dirname(target), {
|
|
670
|
+
recursive: true,
|
|
671
|
+
});
|
|
672
|
+
await atomicWriteFile(target, JSON.stringify(preview, null, 2) + "\n");
|
|
673
|
+
}
|
|
674
|
+
context.output?.result(preview);
|
|
675
|
+
print(context, JSON.stringify(preview, null, 2));
|
|
676
|
+
return preview.complete ? 0 : 3;
|
|
677
|
+
}
|
|
678
|
+
if (sub === "migrate") {
|
|
679
|
+
const { values, positionals } = flags(rest);
|
|
680
|
+
if (positionals.length ||
|
|
681
|
+
!values.from ||
|
|
682
|
+
!values.ownership ||
|
|
683
|
+
Object.keys(values).some((k) => ![
|
|
684
|
+
"from",
|
|
685
|
+
"ownership",
|
|
686
|
+
"out",
|
|
687
|
+
"json",
|
|
688
|
+
].includes(k))) {
|
|
689
|
+
throw new Error("tokens migrate --from LEGACY_JSON --ownership SET_OWNERS_JSON [--out NEW_FILE]");
|
|
690
|
+
}
|
|
691
|
+
const migrated = migrateTokenSnapshot(await readDesignJson(context.root, values.from), (await readDesignJson(context.root, values.ownership)));
|
|
692
|
+
if (values.out) {
|
|
693
|
+
const target = await resolveProjectOutput(context.root, values.out);
|
|
694
|
+
await mkdir(path.dirname(target), {
|
|
695
|
+
recursive: true,
|
|
696
|
+
});
|
|
697
|
+
await atomicWriteFile(target, JSON.stringify(migrated, null, 2) + "\n");
|
|
698
|
+
}
|
|
699
|
+
context.output?.result(migrated);
|
|
700
|
+
print(context, JSON.stringify(migrated, null, 2));
|
|
701
|
+
return 0;
|
|
702
|
+
}
|
|
703
|
+
const commands = {
|
|
704
|
+
snapshot: snapshotCommand,
|
|
705
|
+
check: checkCommand,
|
|
706
|
+
diff: diffCommand,
|
|
707
|
+
push: pushCommand,
|
|
708
|
+
status: statusCommand,
|
|
709
|
+
"verify-modes": (context, args) => modeCommand(context, args, flags(args).positionals[0]),
|
|
710
|
+
};
|
|
225
711
|
const run = commands[sub];
|
|
226
712
|
if (!run) {
|
|
227
|
-
|
|
713
|
+
diagnostic(context, `unknown: penpair marwes tokens ${sub}\nusage:\n ${marwesCommand.usage.join("\n ")}`);
|
|
228
714
|
return 1;
|
|
229
715
|
}
|
|
716
|
+
const parsed = flags(rest);
|
|
717
|
+
const allowed = new Set([
|
|
718
|
+
"json",
|
|
719
|
+
...(sub === "snapshot"
|
|
720
|
+
? [
|
|
721
|
+
"write",
|
|
722
|
+
]
|
|
723
|
+
: sub === "push"
|
|
724
|
+
? [
|
|
725
|
+
"apply",
|
|
726
|
+
"prune",
|
|
727
|
+
"resume",
|
|
728
|
+
"checkpoint",
|
|
729
|
+
"force-lock",
|
|
730
|
+
"trace",
|
|
731
|
+
"overwrite",
|
|
732
|
+
"adopt",
|
|
733
|
+
]
|
|
734
|
+
: sub === "diff"
|
|
735
|
+
? [
|
|
736
|
+
"resolved",
|
|
737
|
+
"apply",
|
|
738
|
+
"resume",
|
|
739
|
+
"checkpoint",
|
|
740
|
+
"force-lock",
|
|
741
|
+
]
|
|
742
|
+
: sub === "verify-modes"
|
|
743
|
+
? [
|
|
744
|
+
"page",
|
|
745
|
+
"out",
|
|
746
|
+
"apply",
|
|
747
|
+
"resume",
|
|
748
|
+
"checkpoint",
|
|
749
|
+
"force-lock",
|
|
750
|
+
]
|
|
751
|
+
: []),
|
|
752
|
+
]);
|
|
753
|
+
if (sub === "verify-modes" ? parsed.positionals.length !== 1 : parsed.positionals.length) {
|
|
754
|
+
throw new Error("Unexpected positional arguments after the token subcommand");
|
|
755
|
+
}
|
|
756
|
+
for (const key of Object.keys(parsed.values)) {
|
|
757
|
+
if (!allowed.has(key)) {
|
|
758
|
+
throw new Error(`--${key} is not supported by tokens ${sub}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (sub === "diff" &&
|
|
762
|
+
!parsed.values.resolved &&
|
|
763
|
+
[
|
|
764
|
+
parsed.values.apply,
|
|
765
|
+
parsed.values.resume,
|
|
766
|
+
parsed.values.checkpoint,
|
|
767
|
+
parsed.values["force-lock"],
|
|
768
|
+
].some(Boolean)) {
|
|
769
|
+
throw new Error("Mode write flags require diff --resolved");
|
|
770
|
+
}
|
|
230
771
|
return run(context, rest);
|
|
231
772
|
},
|
|
232
773
|
};
|