synthesisui 0.16.482 → 0.16.483
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/colour-names.js +52 -0
- package/dist/commands/add.js +38 -18
- package/dist/commands/align.js +13 -4
- package/dist/commands/colours.js +1 -1
- package/dist/commands/sync.js +48 -2
- package/dist/commands/upgrade.js +17 -2
- package/dist/install-marks.js +5 -1
- package/dist/installed-at.js +41 -0
- package/dist/new-version.js +201 -0
- package/dist/repo-state.js +4 -3
- package/package.json +1 -1
package/dist/colour-names.js
CHANGED
|
@@ -204,3 +204,55 @@ export function agentPrompt(plan) {
|
|
|
204
204
|
"Change a value only where it is used as the line says. Leave every other colour as it is, and do not add or invent a name or a colour - these six were chosen by the person.",
|
|
205
205
|
].join("\n");
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* O UPGRADE TRAZ AS SEIS CORES NOVAS - etapa 01 da jornada "cada um sabe se o seu código está em dia", 29/09.
|
|
209
|
+
*
|
|
210
|
+
* Só o `colours` escrevia o bloco, então quem recebia a versão nova de um colega pelo `upgrade` ficava com os valores
|
|
211
|
+
* velhos no CSS global (medido no teste-e09: `--color-primary: #0070f3` com o sistema em `#2e5078`). Aqui o bloco que
|
|
212
|
+
* é NOSSO - entre as marcas - tem os valores trocados pelos da versão nova; nada fora dele é tocado.
|
|
213
|
+
*
|
|
214
|
+
* O papel de cada linha: pela grafia (`--<prefixo>-<palavra>`) e, quando o nome é o DELE (`--accent`), pela cor de
|
|
215
|
+
* antes - é a cor que a versão instalada dava àquele papel.
|
|
216
|
+
*/
|
|
217
|
+
export function refreshColourBlock(css, slug, before, after) {
|
|
218
|
+
const hit = blockMarks.END_RE(slug).exec(css);
|
|
219
|
+
if (!hit)
|
|
220
|
+
return { css, changes: [] };
|
|
221
|
+
const low = (x) => (x ?? "").trim().toLowerCase();
|
|
222
|
+
const changes = [];
|
|
223
|
+
const taken = new Set();
|
|
224
|
+
const block = hit[0].replace(/^(\s*)(--[a-z0-9-]+)(\s*:\s*)([^;]+);/gim, (line, pad, name, colon, value) => {
|
|
225
|
+
const byWord = SIX_ROLES.find((r) => !taken.has(r) &&
|
|
226
|
+
new RegExp(`^--[a-z0-9]+-${WORD[r].word}$`, "i").test(name));
|
|
227
|
+
const role = byWord ??
|
|
228
|
+
SIX_ROLES.find((r) => !taken.has(r) && before[r] && low(before[r]) === low(value));
|
|
229
|
+
if (!role)
|
|
230
|
+
return line;
|
|
231
|
+
taken.add(role);
|
|
232
|
+
const to = after[role];
|
|
233
|
+
if (!to || low(to) === low(value))
|
|
234
|
+
return line;
|
|
235
|
+
changes.push({ role, name, from: value.trim(), to });
|
|
236
|
+
return `${pad}${name}${colon}${to};`;
|
|
237
|
+
});
|
|
238
|
+
if (changes.length === 0)
|
|
239
|
+
return { css, changes };
|
|
240
|
+
return {
|
|
241
|
+
css: css.slice(0, hit.index) + block + css.slice(hit.index + hit[0].length),
|
|
242
|
+
changes,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** O mesmo, no disco: acha o CSS global dele e reescreve só se algum valor mudou. */
|
|
246
|
+
export async function refreshColourBlockOnDisk(root, slug, before, after) {
|
|
247
|
+
const v = await readVocabulary(root, slug);
|
|
248
|
+
if (!v.global)
|
|
249
|
+
return { file: null, changes: [] };
|
|
250
|
+
const path = join(root, v.global);
|
|
251
|
+
const css = await readFile(path, "utf8").catch(() => null);
|
|
252
|
+
if (css == null)
|
|
253
|
+
return { file: null, changes: [] };
|
|
254
|
+
const r = refreshColourBlock(css, slug, before, after);
|
|
255
|
+
if (r.changes.length > 0)
|
|
256
|
+
await writeFile(path, r.css, "utf8");
|
|
257
|
+
return { file: v.global, changes: r.changes };
|
|
258
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { neverMeasured } from "../never-measured.js";
|
|
2
1
|
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
4
3
|
import { agentsToMaintain } from "../agents-chosen.js";
|
|
5
4
|
import { syncClaudeMd } from "../claude-md.js";
|
|
6
5
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
@@ -9,18 +8,19 @@ import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.
|
|
|
9
8
|
import { detectAppDirs } from "../global-sheet.js";
|
|
10
9
|
import { lockReference } from "../group-role.js";
|
|
11
10
|
import { buildGuide } from "../guide.js";
|
|
11
|
+
import { nextInstalledAt, readInstalledAt, writeInstalledAt, } from "../installed-at.js";
|
|
12
12
|
import { censusScope } from "../measured-scope.js";
|
|
13
13
|
import { recallAvailable } from "../memory/availability.js";
|
|
14
|
+
import { neverMeasured } from "../never-measured.js";
|
|
14
15
|
import { nextFontFacts } from "../next-font-weights.js";
|
|
15
16
|
import { body as line, section, snippet } from "../output.js";
|
|
16
17
|
import { fetchDesignSystem } from "../registry.js";
|
|
17
18
|
import { repoStateOf } from "../repo-state.js";
|
|
18
19
|
import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
|
|
19
|
-
import { detectStack } from "../stack.js";
|
|
20
|
+
import { detectStack, resolveDeps } from "../stack.js";
|
|
20
21
|
import { onlyWhatMatched } from "../their-theme.js";
|
|
21
22
|
import { tongueOf } from "../their-tongue.js";
|
|
22
23
|
import { pointTokensAtTheirNames } from "../their-vars.js";
|
|
23
|
-
import { resolveDeps } from "../stack.js";
|
|
24
24
|
import { tracksAnyOf } from "../tracked.js";
|
|
25
25
|
/**
|
|
26
26
|
* QUAL METADE DESTA PASTA UM TIME COMMITA.
|
|
@@ -75,6 +75,11 @@ const IGNORED = [
|
|
|
75
75
|
* turno de quem instalasse faria um arquivo nosso aparecer no `git status` dele.
|
|
76
76
|
*/
|
|
77
77
|
".turn-clock",
|
|
78
|
+
/**
|
|
79
|
+
* A HORA EM QUE A VERSÃO ATERROU NESTE CLONE - ver `installed-at.ts` (29/09). Dentro do `.lock` ela fazia dois upgrades
|
|
80
|
+
* da mesma versão, em branches separadas, darem conflito numa linha só.
|
|
81
|
+
*/
|
|
82
|
+
".fetched-at.json",
|
|
78
83
|
];
|
|
79
84
|
const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
|
|
80
85
|
"# clone is governed; the measurement and the local record are not, because\n" +
|
|
@@ -196,7 +201,10 @@ async function familiesHeNames(root, families) {
|
|
|
196
201
|
const kept = Object.entries(families).filter(([, v]) => {
|
|
197
202
|
if (typeof v !== "string")
|
|
198
203
|
return true;
|
|
199
|
-
const first = v
|
|
204
|
+
const first = v
|
|
205
|
+
.split(",")[0]
|
|
206
|
+
?.trim()
|
|
207
|
+
.replace(/^["']|["']$/g, "") ?? "";
|
|
200
208
|
return !first || /^var\(/.test(first) || css.includes(first.toLowerCase());
|
|
201
209
|
});
|
|
202
210
|
return Object.fromEntries(kept);
|
|
@@ -392,7 +400,9 @@ export async function add(slug, opts) {
|
|
|
392
400
|
* E O SISTEMA QUE NUNCA FOI MEDIDO NUM CÓDIGO já nasce assim - ver `neverMeasured` (rodada 5, 27/09):
|
|
393
401
|
* o fork do leigo-5 terminava o `connect` mandando rodar `sync` para uma pasta que não existe.
|
|
394
402
|
*/
|
|
395
|
-
...(prev?.reading === "none" || neverMeasured(payload.document)
|
|
403
|
+
...(prev?.reading === "none" || neverMeasured(payload.document)
|
|
404
|
+
? { reading: "none" }
|
|
405
|
+
: {}),
|
|
396
406
|
/** Ver `RegistryPayload.compiler`: é o que faz um conserto de CSS chegar a um install. */
|
|
397
407
|
...(payload.compiler != null ? { compiler: payload.compiler } : {}),
|
|
398
408
|
...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
|
|
@@ -428,17 +438,21 @@ export async function add(slug, opts) {
|
|
|
428
438
|
/** A memória do que NÓS escrevemos atravessa a re-materialização - ver `RootLock.written`. */
|
|
429
439
|
...(prev?.written ? { written: prev.written } : {}),
|
|
430
440
|
};
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
+
/**
|
|
442
|
+
* O `.lock` SEM A HORA - ver `installed-at.ts` (29/09): dois clones na mesma versão escrevem o mesmo arquivo, e a hora
|
|
443
|
+
* de cada um vai para `.fetched-at.json`, que o git ignora.
|
|
444
|
+
*/
|
|
445
|
+
const slugDirOfLock = dirname(rootLockPath);
|
|
446
|
+
const installedAt = nextInstalledAt({
|
|
447
|
+
prevLock: prev ??
|
|
448
|
+
null,
|
|
449
|
+
prevAt: await readInstalledAt(slugDirOfLock, null),
|
|
450
|
+
identity: identity,
|
|
451
|
+
now: new Date().toISOString(),
|
|
452
|
+
});
|
|
453
|
+
const lock = { ...identity };
|
|
441
454
|
await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
|
|
455
|
+
await writeInstalledAt(slugDirOfLock, installedAt);
|
|
442
456
|
/**
|
|
443
457
|
* O MAPA SOBE AGORA, e é o único momento em que ele é novo.
|
|
444
458
|
*
|
|
@@ -637,7 +651,9 @@ export async function add(slug, opts) {
|
|
|
637
651
|
* `@theme` - vinte linhas depois de "100% of your 59 style decisions carry your own vocabulary".
|
|
638
652
|
*/
|
|
639
653
|
const wearsTailwind = await projectWearsTailwind(projectRoot);
|
|
640
|
-
if (wearsTailwind &&
|
|
654
|
+
if (wearsTailwind &&
|
|
655
|
+
!opts.themeFollows &&
|
|
656
|
+
(aligned.dropped.length > 0 || aligned.own.length > 0)) {
|
|
641
657
|
console.log(line(
|
|
642
658
|
/**
|
|
643
659
|
* A LINHA DIZ QUE FICARAM DE FORA - e nao AFIRMA por que, porque ela nao sabe.
|
|
@@ -779,7 +795,11 @@ export async function add(slug, opts) {
|
|
|
779
795
|
*/
|
|
780
796
|
const stackDeps = await resolveDeps(projectRoot).catch(() => ({}));
|
|
781
797
|
const onNext = projectConfig.target === "next" && "next" in stackDeps;
|
|
782
|
-
const headHome = onNext
|
|
798
|
+
const headHome = onNext
|
|
799
|
+
? "app/layout.tsx"
|
|
800
|
+
: "vite" in stackDeps
|
|
801
|
+
? "index.html"
|
|
802
|
+
: "your root HTML";
|
|
783
803
|
const fontPlan = onNext
|
|
784
804
|
? nextFontSnippet({
|
|
785
805
|
families,
|
package/dist/commands/align.js
CHANGED
|
@@ -9,6 +9,7 @@ import { unsentEvents } from "../doctor/ledger.js";
|
|
|
9
9
|
import { readRequests } from "../doctor/requests.js";
|
|
10
10
|
import { declaredReference } from "../group-role.js";
|
|
11
11
|
import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
|
|
12
|
+
import { readInstalledAt } from "../installed-at.js";
|
|
12
13
|
import { fmt, say } from "../lang.js";
|
|
13
14
|
import { measuredScope } from "../measured-scope.js";
|
|
14
15
|
import { body, bodyWrapped, paint, section, snippet } from "../output.js";
|
|
@@ -34,7 +35,8 @@ async function wholeRepoMeasured(root) {
|
|
|
34
35
|
return false;
|
|
35
36
|
try {
|
|
36
37
|
const c = JSON.parse(raw);
|
|
37
|
-
const narrowed = (typeof c.scope === "string" && c.scope.trim() !== "") ||
|
|
38
|
+
const narrowed = (typeof c.scope === "string" && c.scope.trim() !== "") ||
|
|
39
|
+
(Array.isArray(c.scopes) && c.scopes.length > 0);
|
|
38
40
|
return c.measured != null && typeof c.measured === "object" && !narrowed;
|
|
39
41
|
}
|
|
40
42
|
catch {
|
|
@@ -66,7 +68,10 @@ async function locksIn(root) {
|
|
|
66
68
|
if (!raw)
|
|
67
69
|
continue;
|
|
68
70
|
try {
|
|
69
|
-
|
|
71
|
+
const lock = JSON.parse(raw);
|
|
72
|
+
/** A hora mora ao lado desde 29/09 - ver `installed-at.ts`; o `.lock` antigo ainda responde. */
|
|
73
|
+
const at = await readInstalledAt(join(dsDir, entry.name), lock);
|
|
74
|
+
out.push(at ? { ...lock, fetchedAt: at } : lock);
|
|
70
75
|
}
|
|
71
76
|
catch {
|
|
72
77
|
// lock ilegível - o `status` já reporta isso com o nome do arquivo
|
|
@@ -228,7 +233,10 @@ opts = {}) {
|
|
|
228
233
|
* sistema de 36).
|
|
229
234
|
*/
|
|
230
235
|
const scope = await measuredScope(root, lock.slug);
|
|
231
|
-
if (!scope.system &&
|
|
236
|
+
if (!scope.system &&
|
|
237
|
+
scope.usage.length === 0 &&
|
|
238
|
+
lock.reading !== "none" &&
|
|
239
|
+
!(await wholeRepoMeasured(root)))
|
|
232
240
|
out.push({
|
|
233
241
|
says: `"${lock.slug}" does not record where it was measured, so a re-measure would read this whole repo instead of the folder the system came from. Sync asks once and remembers the answer.`,
|
|
234
242
|
/**
|
|
@@ -521,6 +529,7 @@ export async function versionBehind(root, opts = {}) {
|
|
|
521
529
|
return repaired;
|
|
522
530
|
if (body.version > lock.version)
|
|
523
531
|
return {
|
|
532
|
+
kind: "version",
|
|
524
533
|
says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
|
|
525
534
|
run: `npx synthesisui upgrade ${lock.slug}`,
|
|
526
535
|
};
|
|
@@ -707,7 +716,7 @@ opts = {}) {
|
|
|
707
716
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
708
717
|
...(opts.home ? { home: opts.home } : {}),
|
|
709
718
|
}).catch(() => null);
|
|
710
|
-
if (remote)
|
|
719
|
+
if (remote && !(opts.skipVersion && remote.kind === "version"))
|
|
711
720
|
items.push(remote);
|
|
712
721
|
return items;
|
|
713
722
|
}
|
package/dist/commands/colours.js
CHANGED
|
@@ -98,7 +98,7 @@ export function markedBefore(before, documentJson) {
|
|
|
98
98
|
return { ...before, ...marked };
|
|
99
99
|
}
|
|
100
100
|
/** As seis cores da versão que está instalada agora - `{}` quando ainda não há nenhuma. */
|
|
101
|
-
async function installedRoles(slugDir) {
|
|
101
|
+
export async function installedRoles(slugDir) {
|
|
102
102
|
const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
|
|
103
103
|
if (typeof lock.version !== "number")
|
|
104
104
|
return {};
|
package/dist/commands/sync.js
CHANGED
|
@@ -13,11 +13,16 @@ import { measuredScope, rememberScope } from "../measured-scope.js";
|
|
|
13
13
|
import { fromCensus } from "../memory/observation.js";
|
|
14
14
|
import { reportMeasurement } from "../memory/report.js";
|
|
15
15
|
import { mergeCensus } from "../merge-census.js";
|
|
16
|
-
import {
|
|
16
|
+
import { readVocabulary } from "../new-project-theme.js";
|
|
17
|
+
import { gitFacts, offerNewVersion } from "../new-version.js";
|
|
17
18
|
import { body, paint, section, snippet } from "../output.js";
|
|
19
|
+
import { fetchDesignSystem } from "../registry.js";
|
|
18
20
|
import { repoStateOf } from "../repo-state.js";
|
|
21
|
+
import { describeGrowth, recordGrowthForTurn, whatGrew } from "../what-grew.js";
|
|
19
22
|
import { reportWhatIsLeft } from "./align.js";
|
|
23
|
+
import { installedRoles, roleColours } from "./colours.js";
|
|
20
24
|
import { resolveReadParts, siblingProjects, takeCensus, } from "./import.js";
|
|
25
|
+
import { upgrade } from "./upgrade.js";
|
|
21
26
|
/**
|
|
22
27
|
* What each decision means ON THIS MACHINE - the card decides, the sync
|
|
23
28
|
* enacts. The web never touches the local file; this line is printed right
|
|
@@ -321,9 +326,50 @@ export async function sync(opts) {
|
|
|
321
326
|
* dizendo o que sobrou resolve o que a dúvida realmente era: acabou ou não? Mudo quando não sobrou
|
|
322
327
|
* nada, que é o caso normal.
|
|
323
328
|
*/
|
|
324
|
-
|
|
329
|
+
/**
|
|
330
|
+
* UMA VERSÃO NOVA, DITA POR EXTENSO - etapa 02 de "cada um sabe se o seu código está em dia" (29/09): o que mudou,
|
|
331
|
+
* e o caminho sem conflito. Quando ela foi dita aqui, a lista do fim não a repete.
|
|
332
|
+
*/
|
|
333
|
+
const offered = await offerNewVersionHere(root, slug, base).catch(() => "current");
|
|
334
|
+
await reportWhatIsLeft(root, offered === "current" ? {} : { skipVersion: true });
|
|
325
335
|
sayRefused(refused);
|
|
326
336
|
}
|
|
337
|
+
async function offerNewVersionHere(root, slug, base) {
|
|
338
|
+
const slugDir = join(root, "_synthesisui", "ds", slug);
|
|
339
|
+
const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8").catch(() => "{}"));
|
|
340
|
+
if (typeof lock.version !== "number")
|
|
341
|
+
return "current";
|
|
342
|
+
const { global } = await readVocabulary(root, slug);
|
|
343
|
+
return offerNewVersion(root, { slug, version: lock.version }, { globalCss: global }, {
|
|
344
|
+
latest: async () => {
|
|
345
|
+
const p = await fetchDesignSystem(base, slug);
|
|
346
|
+
return {
|
|
347
|
+
version: p.version,
|
|
348
|
+
roles: roleColours(p.artifacts["tokens.css"] ?? ""),
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
installedRoles: () => installedRoles(slugDir),
|
|
352
|
+
git: () => gitFacts(root, slug, global),
|
|
353
|
+
ask: askYesNo,
|
|
354
|
+
upgrade: (s) => upgrade(s, { registry: base, dir: root }),
|
|
355
|
+
tty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async function askYesNo(question) {
|
|
359
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
360
|
+
return false;
|
|
361
|
+
const { createInterface } = await import("node:readline/promises");
|
|
362
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
363
|
+
try {
|
|
364
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
rl.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
327
373
|
/**
|
|
328
374
|
* O QUE NÃO FOI ENVIADO, DITO POR ÚLTIMO - e o "por último" é o desenho, não um detalhe.
|
|
329
375
|
*
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { speakInHisProject } from "../in-his-form.js";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { pinnedHookVersion, wireAgent } from "../agent-wiring.js";
|
|
5
4
|
import { agentsToMaintain } from "../agents-chosen.js";
|
|
6
5
|
import { isOlderCli } from "../cli-version.js";
|
|
6
|
+
import { refreshColourBlockOnDisk } from "../colour-names.js";
|
|
7
7
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
8
8
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
9
9
|
import { unsentEvents } from "../doctor/ledger.js";
|
|
10
10
|
import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
|
|
11
11
|
import { detectAppDirs } from "../global-sheet.js";
|
|
12
|
+
import { speakInHisProject } from "../in-his-form.js";
|
|
12
13
|
import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
|
|
13
14
|
import { body, section, snippet } from "../output.js";
|
|
14
15
|
import { reactMajorOf, readInstalledConvention, readInstalledScheme, theirThemeVars, themeVocabOf, } from "../project-facts.js";
|
|
@@ -19,6 +20,7 @@ import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-co
|
|
|
19
20
|
import { editedSinceWritten, keptLine, readWritten, recordWritten, } from "../written.js";
|
|
20
21
|
import { add } from "./add.js";
|
|
21
22
|
import { reportWhatIsLeft } from "./align.js";
|
|
23
|
+
import { installedRoles } from "./colours.js";
|
|
22
24
|
import { doctor } from "./doctor.js";
|
|
23
25
|
/**
|
|
24
26
|
* The highest `v<n>` below `installed` among the folder names given, or the one
|
|
@@ -384,6 +386,8 @@ export async function upgrade(asked, opts) {
|
|
|
384
386
|
console.log(`✓ ${slug} v${installed} is newer than the registry's v${latest.version} - nothing to do.`);
|
|
385
387
|
return;
|
|
386
388
|
}
|
|
389
|
+
/** As seis cores de ANTES - o bloco do CSS global dele guarda estas (etapa 01, 29/09). */
|
|
390
|
+
const coloursBefore = await installedRoles(slugDir);
|
|
387
391
|
// 1. re-materialize the artifacts (v<latest>/ + root re-exports + .lock);
|
|
388
392
|
// setup hints suppressed - an upgrade means the app is already wired.
|
|
389
393
|
await add(slug, {
|
|
@@ -392,6 +396,11 @@ export async function upgrade(asked, opts) {
|
|
|
392
396
|
setupHints: false,
|
|
393
397
|
...(opts.cli ? { cli: opts.cli } : {}),
|
|
394
398
|
});
|
|
399
|
+
/**
|
|
400
|
+
* AS SEIS CORES NOVAS NO CSS GLOBAL DELE - etapa 01 de "cada um sabe se o seu código está em dia", 29/09. Quem
|
|
401
|
+
* recebia a versão de um colega ficava com os valores velhos no bloco que o `colours` escreveu.
|
|
402
|
+
*/
|
|
403
|
+
const colourBlock = await refreshColourBlockOnDisk(root, slug, coloursBefore, await installedRoles(slugDir));
|
|
395
404
|
// 2. regenerate YOUR materialized components (the ones `component` wrote)
|
|
396
405
|
const config = await readProjectConfig(root);
|
|
397
406
|
const regenerated = [];
|
|
@@ -541,6 +550,12 @@ export async function upgrade(asked, opts) {
|
|
|
541
550
|
else {
|
|
542
551
|
console.log(body("No breaking changes detected."));
|
|
543
552
|
}
|
|
553
|
+
if (colourBlock.changes.length > 0) {
|
|
554
|
+
console.log("");
|
|
555
|
+
console.log(body(`✓ ${colourBlock.file}: ${colourBlock.changes.length === 1 ? "1 of your six colours" : `${colourBlock.changes.length} of your six colours`} now ${colourBlock.changes.length === 1 ? "has" : "have"} the new value:`));
|
|
556
|
+
for (const c of colourBlock.changes)
|
|
557
|
+
console.log(body(` ${c.name}: ${c.from} → ${c.to} (${c.role})`));
|
|
558
|
+
}
|
|
544
559
|
if (regenerated.length > 0) {
|
|
545
560
|
console.log("");
|
|
546
561
|
console.log(body(`Regenerated ${regenerated.length} materialized component(s): ${regenerated.join(", ")}`));
|
|
@@ -579,5 +594,5 @@ export async function upgrade(asked, opts) {
|
|
|
579
594
|
* `button-gen`: "Could not regenerate (...) (removed/renamed in v2?)". Sem cabeçalho, a pasta é a melhor pista.
|
|
580
595
|
*/
|
|
581
596
|
export function recipeNameOf(head, entry) {
|
|
582
|
-
return /Generated by SynthesisUI - "([^"]+)" from the/.exec(head)?.[1] ?? entry;
|
|
597
|
+
return (/Generated by SynthesisUI - "([^"]+)" from the/.exec(head)?.[1] ?? entry);
|
|
583
598
|
}
|
package/dist/install-marks.js
CHANGED
|
@@ -310,7 +310,11 @@
|
|
|
310
310
|
* `[color:#ffffff]`, `font-body` em vez de `[font-family:Inter,_sans-serif]`), e a largura em px no degrau vira `w-10`.
|
|
311
311
|
* Um componente escrito antes reprova na própria checagem.
|
|
312
312
|
*/
|
|
313
|
-
|
|
313
|
+
/**
|
|
314
|
+
* 0.16.483 (29/09): o `.lock` sai sem a hora (`fetchedAt` vai para `.fetched-at.json`, ignorado pelo git) - dois
|
|
315
|
+
* upgrades da mesma versão, em branches separadas, davam conflito numa linha só dele. Ver `installed-at.ts`.
|
|
316
|
+
*/
|
|
317
|
+
export const MATERIALISER_SINCE = "0.16.483";
|
|
314
318
|
/**
|
|
315
319
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
316
320
|
*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A HORA EM QUE A VERSÃO ATERROU NESTE CLONE, FORA DO `.lock` - pedido dele em 29/09 ("tira, neste PR").
|
|
5
|
+
*
|
|
6
|
+
* MEDIDO: dois clones trouxeram a mesma versão em branches separadas e o merge parou numa linha só do `.lock` -
|
|
7
|
+
* `"fetchedAt"`, a hora de cada download. O `globals.css`, os tokens e a pasta da versão juntaram limpo. O `.lock` é
|
|
8
|
+
* commitado e o resto dele é função da versão, do compilador e das regras; a hora era a única coisa que descrevia UMA
|
|
9
|
+
* máquina, e é ela que fazia dois upgrades iguais brigarem.
|
|
10
|
+
*
|
|
11
|
+
* Então ela mora num arquivo ao lado, na lista do `.gitignore` que o CLI gerencia - como o `.last-sync.json`. Quem lê
|
|
12
|
+
* (`repairSaid` no align, `installedAt` no repo-state) lê daqui, e do `.lock` antigo quando este arquivo ainda não
|
|
13
|
+
* existe - um repo instalado antes continua respondendo.
|
|
14
|
+
*/
|
|
15
|
+
export const INSTALLED_AT_FILE = ".fetched-at.json";
|
|
16
|
+
/** A hora nova: anda quando algo aterrou, e fica onde estava quando nada mudou - a mesma regra do `.lock` de antes. */
|
|
17
|
+
export function nextInstalledAt(v) {
|
|
18
|
+
const was = v.prevAt ?? v.prevLock?.fetchedAt ?? null;
|
|
19
|
+
if (!v.prevLock || !was)
|
|
20
|
+
return v.now;
|
|
21
|
+
const { fetchedAt: _was, ...before } = v.prevLock;
|
|
22
|
+
const landed = JSON.stringify(before) !== JSON.stringify(v.identity);
|
|
23
|
+
return landed ? v.now : was;
|
|
24
|
+
}
|
|
25
|
+
export async function readInstalledAt(slugDir, lock) {
|
|
26
|
+
const raw = await readFile(join(slugDir, INSTALLED_AT_FILE), "utf8").catch(() => null);
|
|
27
|
+
if (raw) {
|
|
28
|
+
try {
|
|
29
|
+
const at = JSON.parse(raw).fetchedAt;
|
|
30
|
+
if (typeof at === "string")
|
|
31
|
+
return at;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ilegível: cai no `.lock` antigo
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return typeof lock?.fetchedAt === "string" ? lock.fetchedAt : null;
|
|
38
|
+
}
|
|
39
|
+
export async function writeInstalledAt(slugDir, at) {
|
|
40
|
+
await writeFile(join(slugDir, INSTALLED_AT_FILE), `${JSON.stringify({ fetchedAt: at }, null, 2)}\n`, "utf8");
|
|
41
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { body, section, snippet } from "./output.js";
|
|
5
|
+
export function pathFor(f) {
|
|
6
|
+
if (f.remoteVersion != null && f.remoteVersion >= f.latest)
|
|
7
|
+
return "pull";
|
|
8
|
+
if (f.dirty.length > 0)
|
|
9
|
+
return "commit-first";
|
|
10
|
+
return f.tty ? "ask" : "say";
|
|
11
|
+
}
|
|
12
|
+
/** As seis cores que mudaram entre a versão instalada e a publicada, na ordem dos papéis. */
|
|
13
|
+
export function colourChanges(before, after) {
|
|
14
|
+
const low = (x) => (x ?? "").trim().toLowerCase();
|
|
15
|
+
return Object.keys(after)
|
|
16
|
+
.filter((r) => before[r] && after[r] && low(before[r]) !== low(after[r]))
|
|
17
|
+
.map((role) => ({
|
|
18
|
+
role,
|
|
19
|
+
from: before[role],
|
|
20
|
+
to: after[role],
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* O QUADRADO DA COR (29/09, pedido dele olhando o terminal: *"deveria ter um quadrado com as cores ao lado do hex"*). Um
|
|
25
|
+
* bloco com o fundo na cor exata (24 bits); sem terminal, nada - o agente lê texto, e um código de escape é ruído.
|
|
26
|
+
*/
|
|
27
|
+
export function colourSwatch(hex, tty) {
|
|
28
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex.trim());
|
|
29
|
+
if (!tty || !m)
|
|
30
|
+
return "";
|
|
31
|
+
const [r, g, b] = [m[1], m[2], m[3]].map((x) => Number.parseInt(x, 16));
|
|
32
|
+
return `\u001b[48;2;${r};${g};${b}m \u001b[0m`;
|
|
33
|
+
}
|
|
34
|
+
/** O aviso, antes de qualquer escrita: as duas versões e o que mudou nas seis cores. */
|
|
35
|
+
export function newVersionLines(v) {
|
|
36
|
+
const sw = (hex) => {
|
|
37
|
+
const s = v.swatch?.(hex) ?? "";
|
|
38
|
+
return s ? `${s} ` : "";
|
|
39
|
+
};
|
|
40
|
+
const out = [
|
|
41
|
+
`v${v.latest} of "${v.slug}" is published and this repo is on v${v.installed}.`,
|
|
42
|
+
];
|
|
43
|
+
if (v.changes.length > 0) {
|
|
44
|
+
out.push("What changed in your six colours:");
|
|
45
|
+
for (const c of v.changes)
|
|
46
|
+
out.push(` ${c.role.padEnd(11)}${sw(c.from)}${c.from} → ${sw(c.to)}${c.to}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
out.push("None of your six colours changed - the rest (rules, components) is listed in UPGRADE.md once it is here.");
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function run(cmd, args, cwd, timeout = 8000) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
execFile(cmd, args, { cwd, timeout, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, (err, stdout) => resolve(err ? null : stdout.toString()));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* O `.lock` só difere do commit pelo `reading` que o próprio sync anota (o 404 da leitura, em `markNoReading`)?
|
|
60
|
+
* Então a mudança é nossa, e o upgrade a reescreve igual - não é trabalho dele a proteger. Achado no PASS de 29/09.
|
|
61
|
+
*/
|
|
62
|
+
async function onlyOurReading(root, lockPath) {
|
|
63
|
+
const committed = await run("git", ["show", `HEAD:./${lockPath}`], root);
|
|
64
|
+
const now = await readFile(join(root, lockPath), "utf8").catch(() => null);
|
|
65
|
+
if (committed == null || now == null)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const { reading: _a, ...was } = JSON.parse(committed);
|
|
69
|
+
const { reading: _b, ...is } = JSON.parse(now);
|
|
70
|
+
return JSON.stringify(was) === JSON.stringify(is);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function gitFacts(root, slug, globalCss) {
|
|
77
|
+
const none = {
|
|
78
|
+
repo: false,
|
|
79
|
+
origin: false,
|
|
80
|
+
dirty: [],
|
|
81
|
+
remoteVersion: null,
|
|
82
|
+
remoteRef: null,
|
|
83
|
+
gh: false,
|
|
84
|
+
};
|
|
85
|
+
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"], root);
|
|
86
|
+
if (inside?.trim() !== "true")
|
|
87
|
+
return none;
|
|
88
|
+
const paths = [`_synthesisui/ds/${slug}`, ...(globalCss ? [globalCss] : [])];
|
|
89
|
+
const status = (await run("git", ["status", "--porcelain", "--", ...paths], root)) ?? "";
|
|
90
|
+
const lockPath = `_synthesisui/ds/${slug}/.lock`;
|
|
91
|
+
const dirty = [];
|
|
92
|
+
for (const f of status
|
|
93
|
+
.split("\n")
|
|
94
|
+
.map((l) => l.slice(3).trim())
|
|
95
|
+
.filter(Boolean)) {
|
|
96
|
+
if (f === lockPath && (await onlyOurReading(root, lockPath)))
|
|
97
|
+
continue;
|
|
98
|
+
dirty.push(f);
|
|
99
|
+
}
|
|
100
|
+
let remoteVersion = null;
|
|
101
|
+
let remoteRef = null;
|
|
102
|
+
const origin = await run("git", ["remote", "get-url", "origin"], root);
|
|
103
|
+
if (origin) {
|
|
104
|
+
/** O fetch só lê; sem rede ele falha calado e o resto segue sem a informação. */
|
|
105
|
+
await run("git", ["fetch", "--quiet", "origin"], root, 10000);
|
|
106
|
+
const head = (await run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], root))?.trim();
|
|
107
|
+
const candidates = [head, "origin/main", "origin/master"].filter(Boolean);
|
|
108
|
+
const prefix = ((await run("git", ["rev-parse", "--show-prefix"], root)) ?? "").trim();
|
|
109
|
+
for (const ref of candidates) {
|
|
110
|
+
const lock = await run("git", ["show", `${ref}:${prefix}_synthesisui/ds/${slug}/.lock`], root);
|
|
111
|
+
if (lock == null)
|
|
112
|
+
continue;
|
|
113
|
+
remoteRef = ref;
|
|
114
|
+
try {
|
|
115
|
+
const v = JSON.parse(lock).version;
|
|
116
|
+
remoteVersion = typeof v === "number" ? v : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
remoteVersion = null;
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const gh = (await run("gh", ["auth", "status"], root, 5000)) != null;
|
|
125
|
+
return {
|
|
126
|
+
repo: true,
|
|
127
|
+
origin: origin != null,
|
|
128
|
+
dirty,
|
|
129
|
+
remoteVersion,
|
|
130
|
+
remoteRef,
|
|
131
|
+
gh,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* O aviso inteiro, com o caminho escolhido. Devolve o que aconteceu - o `sync` usa para não repetir, na lista do que
|
|
136
|
+
* falta, a linha de versão que acabou de ser dita aqui.
|
|
137
|
+
*/
|
|
138
|
+
export async function offerNewVersion(_root, lock, where, deps, print = (l) => console.log(l)) {
|
|
139
|
+
const latest = await deps.latest().catch(() => null);
|
|
140
|
+
if (!latest || latest.version <= lock.version)
|
|
141
|
+
return "current";
|
|
142
|
+
const changes = colourChanges(await deps.installedRoles(), latest.roles);
|
|
143
|
+
const facts = await deps.git();
|
|
144
|
+
const path = pathFor({
|
|
145
|
+
tty: deps.tty,
|
|
146
|
+
dirty: facts.dirty,
|
|
147
|
+
remoteVersion: facts.remoteVersion,
|
|
148
|
+
latest: latest.version,
|
|
149
|
+
});
|
|
150
|
+
const slug = lock.slug;
|
|
151
|
+
print("");
|
|
152
|
+
print(section(`A new version of ${slug}`));
|
|
153
|
+
for (const l of newVersionLines({
|
|
154
|
+
slug,
|
|
155
|
+
installed: lock.version,
|
|
156
|
+
latest: latest.version,
|
|
157
|
+
changes,
|
|
158
|
+
swatch: (hex) => colourSwatch(hex, deps.tty),
|
|
159
|
+
}))
|
|
160
|
+
print(body(l));
|
|
161
|
+
print("");
|
|
162
|
+
if (path === "pull") {
|
|
163
|
+
const branch = (facts.remoteRef ?? "origin/main").replace(/^origin\//, "");
|
|
164
|
+
print(body(`Someone on your team already brought v${latest.version} into ${facts.remoteRef}. Bring it from git instead of upgrading here - doing the same change twice, in two commits, is what makes a conflict:`));
|
|
165
|
+
print(snippet([`git pull origin ${branch}`]));
|
|
166
|
+
return path;
|
|
167
|
+
}
|
|
168
|
+
if (path === "commit-first") {
|
|
169
|
+
print(body("You have changes not committed where the upgrade writes. Commit or stash them first, so the upgrade lands in a diff of its own:"));
|
|
170
|
+
for (const f of facts.dirty)
|
|
171
|
+
print(body(` ${f}`));
|
|
172
|
+
print("");
|
|
173
|
+
print(body("then:"));
|
|
174
|
+
print(snippet([`npx synthesisui upgrade ${slug}`]));
|
|
175
|
+
return path;
|
|
176
|
+
}
|
|
177
|
+
if (path === "say") {
|
|
178
|
+
print(body("To bring it here:"));
|
|
179
|
+
print(snippet([`npx synthesisui upgrade ${slug}`]));
|
|
180
|
+
return path;
|
|
181
|
+
}
|
|
182
|
+
const yes = await deps.ask(` Bring v${latest.version} into this repo now? It rewrites _synthesisui/ds/${slug}${where.globalCss ? ` and the six colours in ${where.globalCss}` : ""}. [y/N] `);
|
|
183
|
+
if (!yes) {
|
|
184
|
+
print(body("Nothing was changed. When you want it:"));
|
|
185
|
+
print(snippet([`npx synthesisui upgrade ${slug}`]));
|
|
186
|
+
return "declined";
|
|
187
|
+
}
|
|
188
|
+
await deps.upgrade(slug);
|
|
189
|
+
print("");
|
|
190
|
+
print(section("Give it to your team"));
|
|
191
|
+
print(body("It is in your files now, not in git. Commit it on a branch of its own, so whoever pulls gets it without redoing it:"));
|
|
192
|
+
print(snippet([
|
|
193
|
+
`git switch -c synthesisui-${slug}-v${latest.version}`,
|
|
194
|
+
"git status # only what the upgrade wrote should be listed",
|
|
195
|
+
"git add -A",
|
|
196
|
+
`git commit -m "synthesisui: ${slug} v${latest.version}"`,
|
|
197
|
+
...(facts.origin ? ["git push -u origin HEAD"] : []),
|
|
198
|
+
...(facts.origin && facts.gh ? ["gh pr create --fill"] : []),
|
|
199
|
+
]));
|
|
200
|
+
return "upgraded";
|
|
201
|
+
}
|
package/dist/repo-state.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { readFile } from "node:fs/promises";
|
|
27
27
|
import { join } from "node:path";
|
|
28
28
|
import { readEvents, } from "./doctor/ledger.js";
|
|
29
|
+
import { readInstalledAt } from "./installed-at.js";
|
|
29
30
|
/**
|
|
30
31
|
* O estado de um slug neste projeto, ou nada quando não há `.lock`.
|
|
31
32
|
*
|
|
@@ -34,6 +35,8 @@ import { readEvents, } from "./doctor/ledger.js";
|
|
|
34
35
|
*/
|
|
35
36
|
export async function repoStateOf(projectRoot, slug, cli) {
|
|
36
37
|
const lock = await readJson(join(projectRoot, "_synthesisui", "ds", slug, ".lock"));
|
|
38
|
+
/** A hora mora ao lado do `.lock` desde 29/09 - ver `installed-at.ts`. */
|
|
39
|
+
const installedAt = await readInstalledAt(join(projectRoot, "_synthesisui", "ds", slug), lock);
|
|
37
40
|
/**
|
|
38
41
|
* O ÚLTIMO DOCTOR, PELO MESMO LEITOR QUE O `sync` USA (`readEvents`). Abrir o arquivo por conta
|
|
39
42
|
* própria aqui seria uma segunda implementação do formato do registro - e a primeira coisa que
|
|
@@ -93,9 +96,7 @@ export async function repoStateOf(projectRoot, slug, cli) {
|
|
|
93
96
|
? { unrepresentedMatches: doctor.unrepresentedMatches }
|
|
94
97
|
: {}),
|
|
95
98
|
...(runs.length > 0 ? { runs } : {}),
|
|
96
|
-
...(
|
|
97
|
-
? { installedAt: lock.fetchedAt }
|
|
98
|
-
: {}),
|
|
99
|
+
...(installedAt ? { installedAt } : {}),
|
|
99
100
|
};
|
|
100
101
|
/** Só o `cli` não é estado: sem nada do repo, não há o que reportar. */
|
|
101
102
|
return Object.keys(payload).length > 1 ? payload : null;
|
package/package.json
CHANGED