@penpair/marwes-ui 0.2.0 → 0.3.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/dist/build-kit.js +24 -6
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +500 -94
- package/dist/components.js +375 -74
- package/dist/config.js +53 -16
- package/dist/index.js +21 -3
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.js +325 -75
- package/dist/ownership.js +162 -37
- package/dist/penpot.js +447 -114
- package/dist/reconciliation.js +112 -37
- package/dist/resolved-values.d.ts +3 -8
- package/dist/resolved-values.js +99 -103
- package/dist/snapshot-file.js +255 -50
- package/dist/theme-input.js +12 -4
- package/dist/theme-lint.js +113 -27
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +207 -52
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -5,11 +5,11 @@ import { ownershipReport, readSyncAncestor, saveSyncAncestor } from "./ownership
|
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { parseCommandArgs, readDesignJson, atomicWriteFile, resolveProjectOutput, readJournalTarget, guardFile, resolveProjectInput, createCheckpoint, loadConfig, readRawConfig } from "@penpair/core";
|
|
8
|
+
import { projectTheme, parseCommandArgs, readDesignJson, atomicWriteFile, resolveProjectOutput, readJournalTarget, guardFile, resolveProjectInput, createCheckpoint, loadConfig, readRawConfig, } from "@penpair/core";
|
|
9
9
|
import { readMarwesSettings } from "./config.js";
|
|
10
10
|
import { familyProblems, loadInstalledMarwes } from "./marwes.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";
|
|
11
|
+
import { applyPlan, compareToPenpot, describePlan, formatDrift, impactOf, planPush, readLive, } from "./penpot.js";
|
|
12
|
+
import { diffSnapshots, formatDiff, tokenSnapshotSchema, migrateTokenSnapshot, readSnapshot, writeSnapshot, } from "./snapshot-file.js";
|
|
13
13
|
import { loadThemeInput } from "./theme-input.js";
|
|
14
14
|
import { buildSnapshot } from "./tokens.js";
|
|
15
15
|
const print = (context, ...values) => (context.output?.log ?? console.log)(...values);
|
|
@@ -18,25 +18,87 @@ const adapterVersion = createRequire(import.meta.url)("../package.json").version
|
|
|
18
18
|
export async function computeExpectation(root, rawConfig, instance) {
|
|
19
19
|
const settings = readMarwesSettings(rawConfig, instance);
|
|
20
20
|
const installed = await loadInstalledMarwes(await resolveProjectInput(root, settings.project));
|
|
21
|
-
const [light, dark] = await Promise.all([
|
|
21
|
+
const [light, dark] = await Promise.all([
|
|
22
|
+
settings.themes.light,
|
|
23
|
+
settings.themes.dark,
|
|
24
|
+
].map((reference) => (reference ? loadThemeInput(root, reference) : undefined)));
|
|
22
25
|
const fresh = buildSnapshot({
|
|
23
26
|
core: installed.core,
|
|
24
27
|
version: installed.version,
|
|
25
28
|
name: settings.name,
|
|
26
29
|
extras: settings.extras,
|
|
27
|
-
project: {
|
|
30
|
+
project: {
|
|
31
|
+
...(light
|
|
32
|
+
? {
|
|
33
|
+
light: light.input,
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
...(dark
|
|
37
|
+
? {
|
|
38
|
+
dark: dark.input,
|
|
39
|
+
}
|
|
40
|
+
: {}),
|
|
41
|
+
},
|
|
28
42
|
generatedBy: `@penpair/marwes-ui@${adapterVersion}`,
|
|
29
43
|
});
|
|
30
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
settings,
|
|
46
|
+
installed,
|
|
47
|
+
fresh,
|
|
48
|
+
problems: familyProblems(installed),
|
|
49
|
+
};
|
|
31
50
|
}
|
|
32
51
|
export async function checkSnapshotFor(root, rawConfig, instance) {
|
|
33
52
|
const expectation = await computeExpectation(root, rawConfig, instance);
|
|
34
53
|
const committed = await readSnapshot(root, expectation.settings.snapshot);
|
|
35
54
|
const diff = diffSnapshots(committed, expectation.fresh);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
};
|
|
40
102
|
}
|
|
41
103
|
/**
|
|
42
104
|
* For a project's own test suite: resolves the project's penpair.config.json and checks that the committed Marwes token snapshot
|
|
@@ -48,59 +110,145 @@ export async function checkTokenSnapshot(root) {
|
|
|
48
110
|
}
|
|
49
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)}`;
|
|
50
112
|
/** What the snapshot says it does not cover, so a reader is never left thinking everything Marwes provides is in Penpot. */
|
|
51
|
-
const coverage = (snapshot) => (
|
|
52
|
-
|
|
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
|
+
});
|
|
53
170
|
const staleAdvice = "Run `penpair marwes tokens snapshot --write`, review the change in git, and commit it.";
|
|
54
171
|
async function snapshotCommand(context, args) {
|
|
55
172
|
const { values } = flags(args);
|
|
56
173
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
57
174
|
const fresh = check.expectation.fresh;
|
|
58
|
-
if (values.json)
|
|
59
|
-
print(context, JSON.stringify({
|
|
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
|
+
}
|
|
60
184
|
else {
|
|
61
185
|
print(context, `Expected from the installed ${summary(fresh)}`);
|
|
62
|
-
for (const line of formatDiff(check.diff))
|
|
186
|
+
for (const line of formatDiff(check.diff)) {
|
|
63
187
|
print(context, ` ${line}`);
|
|
64
|
-
|
|
188
|
+
}
|
|
189
|
+
if (coverage(fresh)) {
|
|
65
190
|
print(context, `note ${coverage(fresh)}`);
|
|
66
|
-
|
|
191
|
+
}
|
|
192
|
+
for (const problem of check.problems.filter((p) => !p.startsWith("there is no snapshot"))) {
|
|
67
193
|
print(context, `FAIL ${problem}`);
|
|
194
|
+
}
|
|
68
195
|
}
|
|
69
|
-
if (check.expectation.problems.length > 0)
|
|
196
|
+
if (check.expectation.problems.length > 0) {
|
|
70
197
|
return 1;
|
|
198
|
+
}
|
|
71
199
|
if (check.current) {
|
|
72
|
-
if (!values.json)
|
|
200
|
+
if (!values.json) {
|
|
73
201
|
print(context, `ok ${check.expectation.settings.snapshot} is current`);
|
|
202
|
+
}
|
|
74
203
|
return 0;
|
|
75
204
|
}
|
|
76
205
|
if (!values.write) {
|
|
77
|
-
if (!values.json)
|
|
206
|
+
if (!values.json) {
|
|
78
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
|
+
}
|
|
79
209
|
return 2;
|
|
80
210
|
}
|
|
81
211
|
await writeSnapshot(context.root, check.expectation.settings.snapshot, fresh);
|
|
82
|
-
context.output?.result({
|
|
83
|
-
|
|
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) {
|
|
84
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
|
+
}
|
|
85
221
|
return 0;
|
|
86
222
|
}
|
|
87
223
|
async function checkCommand(context, args) {
|
|
88
224
|
const { values } = flags(args);
|
|
89
225
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
90
226
|
if (values.json) {
|
|
91
|
-
print(context, JSON.stringify({
|
|
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));
|
|
92
234
|
return check.current ? 0 : check.expectation.problems.length > 0 ? 1 : 2;
|
|
93
235
|
}
|
|
94
236
|
if (check.current) {
|
|
95
237
|
print(context, `ok ${check.expectation.settings.snapshot} is current (${summary(check.expectation.fresh)})`);
|
|
96
238
|
return 0;
|
|
97
239
|
}
|
|
98
|
-
for (const line of formatDiff(check.diff))
|
|
240
|
+
for (const line of formatDiff(check.diff)) {
|
|
99
241
|
print(context, ` ${line}`);
|
|
100
|
-
|
|
242
|
+
}
|
|
243
|
+
for (const problem of check.problems) {
|
|
101
244
|
print(context, `FAIL ${problem}`);
|
|
102
|
-
|
|
103
|
-
|
|
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";
|
|
104
252
|
print(context, `FAIL the committed snapshot is stale: ${cause}. ${staleAdvice}`);
|
|
105
253
|
}
|
|
106
254
|
return check.expectation.problems.length > 0 ? 1 : 2;
|
|
@@ -108,20 +256,35 @@ async function checkCommand(context, args) {
|
|
|
108
256
|
async function readPenpot(context, committed, wantImpact) {
|
|
109
257
|
return context.withPenpot(async (client) => {
|
|
110
258
|
const target = await readJournalTarget(client);
|
|
111
|
-
const pinned = guardFile(client, {
|
|
259
|
+
const pinned = guardFile(client, {
|
|
260
|
+
id: target.fileId,
|
|
261
|
+
});
|
|
112
262
|
const live = await readLive(pinned);
|
|
113
263
|
const after = await readJournalTarget(pinned);
|
|
114
|
-
if (after.fileId !== target.fileId || after.revision !== target.revision)
|
|
264
|
+
if (after.fileId !== target.fileId || after.revision !== target.revision) {
|
|
115
265
|
throw new Error("File changed during token inspection; inspect again");
|
|
266
|
+
}
|
|
116
267
|
const ancestor = await readSyncAncestor(context.root, target.fileId);
|
|
117
|
-
const drift = compareToPenpot(committed, live, {
|
|
118
|
-
|
|
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
|
+
};
|
|
119
281
|
});
|
|
120
282
|
}
|
|
121
283
|
async function diffCommand(context, args) {
|
|
122
284
|
const { values } = flags(args);
|
|
123
|
-
if (values.resolved)
|
|
285
|
+
if (values.resolved) {
|
|
124
286
|
return modeCommand(context, args);
|
|
287
|
+
}
|
|
125
288
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
126
289
|
if (!check.committed) {
|
|
127
290
|
diagnostic(context, `FAIL ${check.problems.join("; ")}`);
|
|
@@ -129,41 +292,76 @@ async function diffCommand(context, args) {
|
|
|
129
292
|
}
|
|
130
293
|
const { drift, impact } = await readPenpot(context, check.committed, true);
|
|
131
294
|
if (values.json) {
|
|
132
|
-
print(context, JSON.stringify({
|
|
295
|
+
print(context, JSON.stringify({
|
|
296
|
+
snapshotCurrent: check.current,
|
|
297
|
+
drift,
|
|
298
|
+
impact,
|
|
299
|
+
}, null, 2));
|
|
133
300
|
return drift.inSync ? 0 : 2;
|
|
134
301
|
}
|
|
135
|
-
if (!check.current)
|
|
302
|
+
if (!check.current) {
|
|
136
303
|
print(context, `note: the committed snapshot is stale against the installed Marwes (${staleAdvice}); the comparison below is against the committed one.`);
|
|
304
|
+
}
|
|
137
305
|
if (drift.inSync) {
|
|
138
|
-
for (const line of formatDrift(drift, impact))
|
|
306
|
+
for (const line of formatDrift(drift, impact)) {
|
|
139
307
|
print(context, `note preserved: ${line}`);
|
|
308
|
+
}
|
|
140
309
|
print(context, `ok Penpot matches ${summary(check.committed)}`);
|
|
141
310
|
return 0;
|
|
142
311
|
}
|
|
143
|
-
print(context, values.resume
|
|
144
|
-
|
|
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)) {
|
|
145
316
|
print(context, ` ${line}`);
|
|
317
|
+
}
|
|
146
318
|
print(context, "`penpair marwes tokens push --apply` brings Penpot to the snapshot (designer edits require explicit adoption or overwrite).");
|
|
147
319
|
return 2;
|
|
148
320
|
}
|
|
149
321
|
async function modeCommand(context, args, frameId) {
|
|
150
322
|
const { values } = flags(args);
|
|
151
|
-
if ((values.resume || values.checkpoint || values["force-lock"]
|
|
323
|
+
if ((values.resume || values.checkpoint || values["force-lock"] || values.trace) &&
|
|
324
|
+
!values.apply) {
|
|
152
325
|
throw new Error("Mode resume/checkpoint/force-lock require --apply");
|
|
153
|
-
|
|
326
|
+
}
|
|
327
|
+
if (frameId && (!values.page || !values.out)) {
|
|
154
328
|
throw new Error("verify-modes requires one frame ID, --page and --out");
|
|
155
|
-
|
|
329
|
+
}
|
|
330
|
+
if (values.out) {
|
|
156
331
|
await resolveProjectOutput(context.root, values.out);
|
|
332
|
+
}
|
|
157
333
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
158
334
|
if (!check.committed || !check.current) {
|
|
159
335
|
diagnostic(context, "Mode verification requires a current committed snapshot. " + staleAdvice);
|
|
160
336
|
return 2;
|
|
161
337
|
}
|
|
162
|
-
const report = await context.withPenpot(client => runModeTrial(client, check.committed, {
|
|
163
|
-
root: context.root,
|
|
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,
|
|
164
345
|
requireCheckpoint: values.checkpoint === true,
|
|
165
|
-
...(values.checkpoint
|
|
166
|
-
|
|
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
|
+
: {}),
|
|
167
365
|
}));
|
|
168
366
|
context.output?.result(report);
|
|
169
367
|
print(context, JSON.stringify(report, null, 2));
|
|
@@ -171,89 +369,167 @@ async function modeCommand(context, args, frameId) {
|
|
|
171
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.");
|
|
172
370
|
return 0;
|
|
173
371
|
}
|
|
174
|
-
return report.uncertain
|
|
372
|
+
return report.uncertain
|
|
373
|
+
? 4
|
|
374
|
+
: !report.restoration.verified
|
|
375
|
+
? 1
|
|
376
|
+
: !report.complete
|
|
377
|
+
? 3
|
|
378
|
+
: report.verified
|
|
379
|
+
? 0
|
|
380
|
+
: 2;
|
|
175
381
|
}
|
|
176
382
|
async function pushCommand(context, args) {
|
|
177
383
|
const { values } = flags(args);
|
|
178
|
-
if ((values.resume || values.checkpoint || values["force-lock"]
|
|
384
|
+
if ((values.resume || values.checkpoint || values["force-lock"] || values.trace) &&
|
|
385
|
+
!values.apply) {
|
|
179
386
|
throw new Error("Resume/checkpoint require --apply; inspect the journal and current design first");
|
|
387
|
+
}
|
|
180
388
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
181
389
|
if (!check.committed) {
|
|
182
390
|
diagnostic(context, `FAIL ${check.problems.join("; ")}`);
|
|
183
391
|
return 1;
|
|
184
392
|
}
|
|
185
393
|
if (!check.current) {
|
|
186
|
-
for (const line of formatDiff(check.diff))
|
|
394
|
+
for (const line of formatDiff(check.diff)) {
|
|
187
395
|
print(context, ` ${line}`);
|
|
396
|
+
}
|
|
188
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.`);
|
|
189
398
|
return 2;
|
|
190
399
|
}
|
|
191
400
|
const { drift, impact, live, target } = await readPenpot(context, check.committed, true);
|
|
192
|
-
if (values.adopt && (values.apply || values.overwrite || values.resume))
|
|
401
|
+
if (values.adopt && (values.apply || values.overwrite || values.resume)) {
|
|
193
402
|
throw new Error("--adopt writes a local review proposal and cannot be combined with apply/overwrite/resume");
|
|
403
|
+
}
|
|
194
404
|
const ancestor = await readSyncAncestor(context.root, target.fileId);
|
|
195
405
|
const ownership = ownershipReport(check.committed, live, ancestor);
|
|
196
406
|
if (values.adopt) {
|
|
197
|
-
const proposal = JSON.stringify({
|
|
198
|
-
|
|
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) {
|
|
199
417
|
throw new Error("Adoption evidence requires credential redaction; no proposal was published");
|
|
418
|
+
}
|
|
200
419
|
const out = await resolveProjectOutput(context.root, values.adopt);
|
|
201
|
-
await mkdir(path.dirname(out), {
|
|
420
|
+
await mkdir(path.dirname(out), {
|
|
421
|
+
recursive: true,
|
|
422
|
+
});
|
|
202
423
|
await atomicWriteFile(out, proposal);
|
|
203
|
-
context.output?.result({
|
|
424
|
+
context.output?.result({
|
|
425
|
+
phase: "adoption-proposed",
|
|
426
|
+
file: values.adopt,
|
|
427
|
+
ownership,
|
|
428
|
+
complete: false,
|
|
429
|
+
});
|
|
204
430
|
print(context, "Saved adoption proposal. Review declarations and regenerate the snapshot before pushing; no Penpot write or ancestor update occurred.");
|
|
205
431
|
return 3;
|
|
206
432
|
}
|
|
207
|
-
const plan = planPush(check.committed, live, {
|
|
433
|
+
const plan = planPush(check.committed, live, {
|
|
434
|
+
prune: values.prune === true,
|
|
435
|
+
ancestor,
|
|
436
|
+
});
|
|
208
437
|
if (ownership.blockers.length && !values.overwrite && !values.resume) {
|
|
209
|
-
context.output?.result({
|
|
438
|
+
context.output?.result({
|
|
439
|
+
phase: "conflict",
|
|
440
|
+
ownership,
|
|
441
|
+
plan,
|
|
442
|
+
verified: false,
|
|
443
|
+
});
|
|
210
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.`);
|
|
211
445
|
return 2;
|
|
212
446
|
}
|
|
213
447
|
if (drift.inSync && plan.length === 0 && !values.resume) {
|
|
214
448
|
const evidence = JSON.stringify(check.committed);
|
|
215
|
-
if (context.redact && context.redact(evidence) !== evidence)
|
|
449
|
+
if (context.redact && context.redact(evidence) !== evidence) {
|
|
216
450
|
throw new Error("Sync evidence requires credential redaction");
|
|
217
|
-
|
|
451
|
+
}
|
|
452
|
+
if (values.apply) {
|
|
218
453
|
await saveSyncAncestor(context.root, target.fileId, check.committed, ancestor);
|
|
219
|
-
|
|
454
|
+
}
|
|
455
|
+
context.output?.result({
|
|
456
|
+
verified: true,
|
|
457
|
+
unchanged: true,
|
|
458
|
+
drift,
|
|
459
|
+
ownership,
|
|
460
|
+
});
|
|
220
461
|
print(context, `ok Penpot already matches ${summary(check.committed)}; nothing to write`);
|
|
221
462
|
return 0;
|
|
222
463
|
}
|
|
223
|
-
print(context, values.resume
|
|
224
|
-
|
|
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)) {
|
|
225
468
|
print(context, ` ${line}`);
|
|
469
|
+
}
|
|
226
470
|
print(context, "Plan:");
|
|
227
|
-
for (const line of describePlan(plan))
|
|
471
|
+
for (const line of describePlan(plan)) {
|
|
228
472
|
print(context, ` ${line}`);
|
|
473
|
+
}
|
|
229
474
|
if (!values.apply) {
|
|
230
|
-
context.output?.result({
|
|
475
|
+
context.output?.result({
|
|
476
|
+
phase: "inspected",
|
|
477
|
+
ownership,
|
|
478
|
+
plan,
|
|
479
|
+
drift,
|
|
480
|
+
snapshotCurrent: check.current,
|
|
481
|
+
verified: false,
|
|
482
|
+
});
|
|
231
483
|
print(context, "Nothing was written. Run again with --apply to do it.");
|
|
232
|
-
if (!values.prune && drift.sets.some((s) => s.tokens.some((t) => t.kind === "extra")))
|
|
484
|
+
if (!values.prune && drift.sets.some((s) => s.tokens.some((t) => t.kind === "extra"))) {
|
|
233
485
|
print(context, "Tokens without a verified ownership ancestor are preserved, including with --prune.");
|
|
486
|
+
}
|
|
234
487
|
return 0;
|
|
235
488
|
}
|
|
236
489
|
const report = await context.withPenpot((client) => applyPlan(client, check.committed, plan, (message) => context.log(message), {
|
|
237
|
-
prune: values.prune === true,
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
: {}),
|
|
240
512
|
},
|
|
241
513
|
}));
|
|
242
514
|
context.output?.result(report);
|
|
243
|
-
for (const step of report.steps)
|
|
515
|
+
for (const step of report.steps) {
|
|
244
516
|
print(context, `done ${step}`);
|
|
517
|
+
}
|
|
245
518
|
if (report.verified) {
|
|
246
519
|
const evidence = JSON.stringify(check.committed);
|
|
247
|
-
if (context.redact && context.redact(evidence) !== evidence)
|
|
520
|
+
if (context.redact && context.redact(evidence) !== evidence) {
|
|
248
521
|
throw new Error("Sync evidence requires credential redaction");
|
|
522
|
+
}
|
|
249
523
|
await saveSyncAncestor(context.root, target.fileId, check.committed, ancestor);
|
|
250
524
|
print(context, `ok verified: Penpot now matches the snapshot and no shape moved or was rebound (${report.structureAfter.count} shapes unchanged)`);
|
|
251
525
|
return 0;
|
|
252
526
|
}
|
|
253
|
-
if (!report.structureUnchanged)
|
|
527
|
+
if (!report.structureUnchanged) {
|
|
254
528
|
diagnostic(context, `FAIL the shapes changed while pushing tokens (${report.structureBefore.hash} -> ${report.structureAfter.hash}); look at the file before going on`);
|
|
255
|
-
|
|
529
|
+
}
|
|
530
|
+
for (const line of formatDrift(report.drift)) {
|
|
256
531
|
diagnostic(context, `FAIL still different: ${line}`);
|
|
532
|
+
}
|
|
257
533
|
return 1;
|
|
258
534
|
}
|
|
259
535
|
async function statusCommand(context, args) {
|
|
@@ -261,34 +537,54 @@ async function statusCommand(context, args) {
|
|
|
261
537
|
const check = await checkSnapshotFor(context.root, context.rawConfig, context.config.instance);
|
|
262
538
|
const lines = [];
|
|
263
539
|
let code = 0;
|
|
264
|
-
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(", ")}`);
|
|
265
543
|
for (const problem of check.expectation.problems) {
|
|
266
544
|
lines.push(`FAIL ${problem}`);
|
|
267
545
|
code = 1;
|
|
268
546
|
}
|
|
269
|
-
lines.push(check.committed
|
|
270
|
-
|
|
271
|
-
|
|
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) {
|
|
272
554
|
code = 2;
|
|
555
|
+
}
|
|
273
556
|
let drift;
|
|
274
557
|
if (check.committed) {
|
|
275
558
|
try {
|
|
276
559
|
drift = (await readPenpot(context, check.committed, false)).drift;
|
|
277
|
-
lines.push(drift.inSync
|
|
278
|
-
|
|
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) {
|
|
279
564
|
code = 2;
|
|
565
|
+
}
|
|
280
566
|
}
|
|
281
567
|
catch (error) {
|
|
282
|
-
if (code === 0)
|
|
568
|
+
if (code === 0) {
|
|
283
569
|
code = 3;
|
|
570
|
+
}
|
|
284
571
|
lines.push(`skip Penpot: not checked (${String(error.message).split("\n")[0]})`);
|
|
285
572
|
}
|
|
286
573
|
}
|
|
287
|
-
if (values.json)
|
|
288
|
-
print(context, JSON.stringify({
|
|
289
|
-
|
|
290
|
-
|
|
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) {
|
|
291
585
|
print(context, line);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
292
588
|
return code;
|
|
293
589
|
}
|
|
294
590
|
export const marwesCommand = {
|
|
@@ -298,6 +594,8 @@ export const marwesCommand = {
|
|
|
298
594
|
"penpair marwes theme lint [--json] advisory family/default hue gaps; no design writes",
|
|
299
595
|
"penpair marwes tokens schema | migrate --from FILE --ownership FILE [--out NEW_FILE] schema and explicit legacy migration",
|
|
300
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",
|
|
301
599
|
"penpair marwes tokens check [--json] offline, for CI: is the committed snapshot current? (exit 2 when stale)",
|
|
302
600
|
"penpair marwes tokens diff [--json] Penpot against the snapshot: missing, changed, added by hand, and what each change reaches",
|
|
303
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",
|
|
@@ -309,59 +607,167 @@ export const marwesCommand = {
|
|
|
309
607
|
async run(context, args) {
|
|
310
608
|
const [area, sub, ...rest] = args;
|
|
311
609
|
if (area === "theme" && sub === "lint") {
|
|
312
|
-
if (rest.some(value => value !== "--json"))
|
|
610
|
+
if (rest.some((value) => value !== "--json")) {
|
|
313
611
|
throw new Error("theme lint accepts only --json");
|
|
612
|
+
}
|
|
314
613
|
const expected = await computeExpectation(context.root, context.rawConfig, context.config.instance);
|
|
315
|
-
if (expected.problems.length)
|
|
614
|
+
if (expected.problems.length) {
|
|
316
615
|
throw new Error(expected.problems.join("; "));
|
|
317
|
-
|
|
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
|
+
});
|
|
318
623
|
const report = lintThemeFamily(defaults, expected.fresh, expected.settings.themeLint);
|
|
319
624
|
context.output?.result(report);
|
|
320
625
|
print(context, JSON.stringify(report, null, 2));
|
|
321
626
|
return 0;
|
|
322
627
|
}
|
|
323
|
-
if (area === "components")
|
|
628
|
+
if (area === "components") {
|
|
324
629
|
return componentsCommand(context, args.slice(1));
|
|
630
|
+
}
|
|
325
631
|
if (area !== "tokens" || !sub) {
|
|
326
632
|
diagnostic(context, `usage:\n ${marwesCommand.usage.join("\n ")}`);
|
|
327
633
|
return 1;
|
|
328
634
|
}
|
|
329
635
|
if (sub === "schema") {
|
|
330
|
-
if (rest.some(value => value !== "--json"))
|
|
636
|
+
if (rest.some((value) => value !== "--json")) {
|
|
331
637
|
throw new Error("tokens schema accepts only --json");
|
|
638
|
+
}
|
|
332
639
|
context.output?.result(tokenSnapshotSchema);
|
|
333
640
|
print(context, JSON.stringify(tokenSnapshotSchema, null, 2));
|
|
334
641
|
return 0;
|
|
335
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
|
+
}
|
|
336
678
|
if (sub === "migrate") {
|
|
337
679
|
const { values, positionals } = flags(rest);
|
|
338
|
-
if (positionals.length ||
|
|
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))) {
|
|
339
689
|
throw new Error("tokens migrate --from LEGACY_JSON --ownership SET_OWNERS_JSON [--out NEW_FILE]");
|
|
340
|
-
|
|
690
|
+
}
|
|
691
|
+
const migrated = migrateTokenSnapshot(await readDesignJson(context.root, values.from), (await readDesignJson(context.root, values.ownership)));
|
|
341
692
|
if (values.out) {
|
|
342
693
|
const target = await resolveProjectOutput(context.root, values.out);
|
|
343
|
-
await mkdir(path.dirname(target), {
|
|
694
|
+
await mkdir(path.dirname(target), {
|
|
695
|
+
recursive: true,
|
|
696
|
+
});
|
|
344
697
|
await atomicWriteFile(target, JSON.stringify(migrated, null, 2) + "\n");
|
|
345
698
|
}
|
|
346
699
|
context.output?.result(migrated);
|
|
347
700
|
print(context, JSON.stringify(migrated, null, 2));
|
|
348
701
|
return 0;
|
|
349
702
|
}
|
|
350
|
-
const commands = {
|
|
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
|
+
};
|
|
351
711
|
const run = commands[sub];
|
|
352
712
|
if (!run) {
|
|
353
713
|
diagnostic(context, `unknown: penpair marwes tokens ${sub}\nusage:\n ${marwesCommand.usage.join("\n ")}`);
|
|
354
714
|
return 1;
|
|
355
715
|
}
|
|
356
716
|
const parsed = flags(rest);
|
|
357
|
-
const allowed = new Set([
|
|
358
|
-
|
|
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) {
|
|
359
754
|
throw new Error("Unexpected positional arguments after the token subcommand");
|
|
360
|
-
|
|
361
|
-
|
|
755
|
+
}
|
|
756
|
+
for (const key of Object.keys(parsed.values)) {
|
|
757
|
+
if (!allowed.has(key)) {
|
|
362
758
|
throw new Error(`--${key} is not supported by tokens ${sub}`);
|
|
363
|
-
|
|
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)) {
|
|
364
769
|
throw new Error("Mode write flags require diff --resolved");
|
|
770
|
+
}
|
|
365
771
|
return run(context, rest);
|
|
366
772
|
},
|
|
367
773
|
};
|