synthesisui 0.16.241 → 0.16.243

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.
@@ -5,8 +5,10 @@ import { pinnedHookVersion } from "../agent-wiring.js";
5
5
  import { isOlderCli } from "../cli-version.js";
6
6
  import { readToken, resolveRegistry } from "../config.js";
7
7
  import { unsentEvents } from "../doctor/ledger.js";
8
+ import { readRequests } from "../doctor/requests.js";
8
9
  import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
9
10
  import { measuredScope } from "../measured-scope.js";
11
+ import { body } from "../output.js";
10
12
  import { SKILLS } from "../skills.js";
11
13
  /**
12
14
  * QUAL CLI MEDIU O CENSO EM DISCO - e era o `reader`, um inteiro, até 11/08.
@@ -313,6 +315,32 @@ export function repairSaid(body, lock) {
313
315
  then: "claude --continue --dangerously-skip-permissions",
314
316
  };
315
317
  }
318
+ /**
319
+ * QUANTAS RESPOSTAS ESTÃO ESPERANDO PARA SEREM BUSCADAS - o cruzamento das duas listas.
320
+ *
321
+ * A plataforma manda os ids que ela já decidiu; o arquivo local diz o que ainda está aberto AQUI.
322
+ * A interseção é a notícia: pedidos que foram respondidos e cuja resposta esta máquina nunca viu.
323
+ *
324
+ * `null` quando não há nada, quando o arquivo não existe, ou quando a plataforma é velha demais para
325
+ * mandar o campo - um CLI novo contra um servidor antigo fica exatamente como estava, calado.
326
+ */
327
+ async function decidedWaiting(root, decided) {
328
+ if (!decided || decided.length === 0)
329
+ return null;
330
+ const open = await readRequests(root).catch(() => []);
331
+ if (open.length === 0)
332
+ return null;
333
+ const answered = new Set(decided);
334
+ const n = open.filter((r) => answered.has(r.id)).length;
335
+ if (n === 0)
336
+ return null;
337
+ return {
338
+ says: n === 1
339
+ ? "A request you filed has been answered on the platform, and this machine has not picked it up yet."
340
+ : `${n} requests you filed have been answered on the platform, and this machine has not picked them up yet.`,
341
+ run: "npx synthesisui sync",
342
+ };
343
+ }
316
344
  export async function versionBehind(root, opts = {}) {
317
345
  const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
318
346
  const lock = locks[0];
@@ -350,6 +378,24 @@ export async function versionBehind(root, opts = {}) {
350
378
  * a versão é a consequência - dizer a consequência primeiro manda a pessoa rodar o comando sem
351
379
  * saber por quê.
352
380
  */
381
+ /**
382
+ * ALGUÉM JÁ RESPONDEU UM PEDIDO SEU, E VOCÊ NÃO TEM COMO SABER.
383
+ *
384
+ * O agente arquiva a lacuna, a pessoa decide na plataforma - e a decisão fica lá. O arquivo local
385
+ * nunca é tocado por design: o próximo `sync` lê o status, fecha o pedido na máquina e imprime o
386
+ * próximo passo. O buraco está no "próximo": até alguém rodar o comando, uma resposta dada há dias
387
+ * não existe deste lado, e quem esperava por ela descobre por acaso.
388
+ *
389
+ * O cruzamento tem que ser AQUI, e é por isso que a plataforma manda ids em vez de uma frase: só
390
+ * esta máquina sabe o que ainda está ABERTO na fila local. Um pedido decidido que já foi fechado
391
+ * aqui não é notícia, e contá-lo faria o hook cobrar para sempre um trabalho já feito.
392
+ *
393
+ * Vem antes da versão e do reparo porque é a única linha desta lista sobre uma pergunta que a
394
+ * PESSOA fez - e uma resposta que ela pediu vale mais que uma novidade que ela não pediu.
395
+ */
396
+ const waiting = await decidedWaiting(root, body.decided);
397
+ if (waiting)
398
+ return waiting;
353
399
  const repaired = repairSaid(body, lock);
354
400
  if (repaired)
355
401
  return repaired;
@@ -486,17 +532,54 @@ export async function misalignments(root, opts = {}) {
486
532
  items.push(remote);
487
533
  return items;
488
534
  }
535
+ /**
536
+ * O ESTADO DE QUEM AINDA NÃO TEM SISTEMA - o silêncio que custava a jornada inteira.
537
+ *
538
+ * `localMisalignments` devolve lista VAZIA quando não há sistema instalado, e a razão está escrita
539
+ * lá em cima: um repo sem DS não está desalinhado, ele só não começou. Correto - e o efeito é que a
540
+ * pessoa que acabou de rodar `connect`, que é exatamente quem mais precisa de uma instrução, abre a
541
+ * primeira sessão do agente em silêncio absoluto. A frase do próximo passo mora hoje na aba do
542
+ * browser que ela acabou de deixar; se fechou a aba, ou voltou no dia seguinte, não existe mais
543
+ * nada dizendo o que fazer.
544
+ *
545
+ * Então isto não é um desalinho e não entra naquela lista: é uma frase de ESTADO, e ela fala em
546
+ * forma de CONVERSA. "Peça ao seu agente: importe meu design system" é executável por quem está
547
+ * lendo - um comando decorado não é, e a esta altura ela nem sabe que comandos existem.
548
+ *
549
+ * DOIS ESTADOS, e não três. O terceiro que o plano previa - "instalado mas não fiado" - ficou de
550
+ * fora com motivo medido: a única leitura confiável de fiação (`readWiring`, no doctor) varre o
551
+ * repositório inteiro, e isto roda em TODA abertura de sessão. Um aviso que custa um walk completo
552
+ * a cada sessão é um aviso que alguém desliga.
553
+ */
554
+ export async function nextStepFor(root) {
555
+ const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
556
+ if (locks.length > 0)
557
+ return null;
558
+ const measured = await readFile(join(root, "_synthesisui", "census.json"), "utf8").then(() => true, () => false);
559
+ return measured
560
+ ? 'This repo has been measured but has no design system yet. Ask me: "continue the import."'
561
+ : 'This repo has no design system contract yet. Ask me: "import my design system."';
562
+ }
489
563
  /**
490
564
  * A CAUDA DE UM COMANDO QUE TERMINOU: o que ainda está fora, ou nada.
491
565
  *
492
566
  * Muda quando não falta nada, que é o caso normal e é o que a mantém legível. Ela existe porque
493
567
  * `sync`, `connect` e `upgrade` deixavam a pessoa sem saber se tinha acabado - e a resposta exigia
494
568
  * lembrar de um sexto comando (dono, 07/08).
569
+ *
570
+ * E QUANDO NÃO HÁ SISTEMA NENHUM ela dizia menos ainda: nada. O `connect` terminava mudo no único
571
+ * repo em que ele é a primeira coisa que alguém roda.
495
572
  */
496
573
  export async function reportWhatIsLeft(root, opts = {}) {
497
574
  const items = await misalignments(root, opts).catch(() => []);
498
- if (items.length === 0)
575
+ if (items.length === 0) {
576
+ const next = await nextStepFor(root).catch(() => null);
577
+ if (next) {
578
+ console.log("");
579
+ console.log(body(next));
580
+ }
499
581
  return;
582
+ }
500
583
  console.log("");
501
584
  console.log(describeMisalignments(items, "after"));
502
585
  }
@@ -506,6 +589,21 @@ export async function align(opts) {
506
589
  ...(opts.cli ? { cli: opts.cli } : {}),
507
590
  });
508
591
  const text = describeMisalignments(items, opts.shell ? "shell" : "session");
509
- if (text)
592
+ if (text) {
510
593
  console.log(text);
594
+ return;
595
+ }
596
+ /**
597
+ * A PRIMEIRA SESSÃO DEPOIS DO `connect` PASSA A TER UMA PRIMEIRA FRASE.
598
+ *
599
+ * Este é o hook `SessionStart`, e até aqui ele só falava de desalinho - então num repo sem
600
+ * sistema ele nunca falava. É onde a tese da landing vira experiência ou não vira nada: o agente
601
+ * abre sabendo o que falta, e quem está lendo descobre o que pedir sem ter guardado nada.
602
+ *
603
+ * Só quando há algo a dizer. O silêncio de um ambiente pronto é a razão de alguém ainda ler esta
604
+ * linha na vez em que ela aparece.
605
+ */
606
+ const next = await nextStepFor(root).catch(() => null);
607
+ if (next)
608
+ console.log(next);
511
609
  }
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
2
+ import { body, section, snippet } from "../output.js";
3
+ import { wiringPrompt } from "../wiring-prompt.js";
2
4
  import { add } from "./add.js";
3
5
  /**
4
6
  * Bootstraps a project for SynthesisUI: writes `_synthesisui/config.json`
@@ -40,11 +42,37 @@ export async function init(opts) {
40
42
  // + rules + CLAUDE.md all arrive via `add`).
41
43
  if (opts.ds) {
42
44
  console.log("");
43
- await add(opts.ds, { registry: opts.registry, dir: root });
44
- // The layers exist and nothing installs them unless this is said out loud.
45
+ /**
46
+ * O FECHO DEIXA DE SER UMA LISTA DE TAREFAS (etapa 1.4 do plano).
47
+ *
48
+ * `add` imprime "One-time setup (once per app)" com três passos numerados
49
+ * para a PESSOA fazer à mão: os dois `@import`, o `data-ds` na raiz e a
50
+ * fiação das fontes. É onde 27/07 mediu 0% de fiação - ninguém fazia, e o
51
+ * sistema que acabara de chegar não aparecia na tela.
52
+ *
53
+ * Quem roda este comando tem um agente aberto do lado. Então o setup
54
+ * manual sai (`setupHints: false`) e no lugar dele vai a coisa que se
55
+ * cola - o mesmo texto que a tela do onboarding entrega, gêmeo por spec.
56
+ * As edições continuam existindo; elas só mudam de mão.
57
+ */
58
+ await add(opts.ds, {
59
+ registry: opts.registry,
60
+ dir: root,
61
+ setupHints: false,
62
+ });
63
+ console.log("");
64
+ console.log(section("Two steps left, and neither is a file you edit"));
65
+ console.log(body("1. Turn the check and the tools on - run this BEFORE you open your agent,"));
66
+ console.log(body(" because hooks and tools are read when a session starts:"));
67
+ console.log("");
68
+ console.log(snippet(["npx synthesisui@latest connect"]));
69
+ console.log("");
70
+ console.log(body("2. Open your agent and paste this once - it does the wiring:"));
71
+ console.log("");
72
+ console.log(snippet(wiringPrompt(opts.ds).split("\n")));
45
73
  console.log("");
46
- console.log(" • synthesisui connect so the check runs on its own, and your agent");
47
- console.log(" can ask this system instead of guessing");
74
+ console.log(body("Then ask it for something real. The check introduces itself on the first"));
75
+ console.log(body("clean file and goes quiet after that."));
48
76
  return;
49
77
  }
50
78
  console.log("");
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { release } from "node:os";
3
- import { resolveRegistry, writeToken } from "../config.js";
3
+ import { readCredentials, resolveRegistry, sameRegistry, writeToken, } from "../config.js";
4
4
  import { RegistryError } from "../registry.js";
5
5
  const CLIENT_ID = "synthesisui-cli";
6
6
  const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
@@ -34,6 +34,29 @@ function openBrowser(url) {
34
34
  /** Device authorization (RFC 8628): opens the browser, waits for approval. */
35
35
  export async function login(opts) {
36
36
  const base = resolveRegistry(opts.registry);
37
+ /**
38
+ * O LOGIN QUE RECONHECE QUEM JÁ ENTROU (etapa 1.5 do plano, vazamento T1.6).
39
+ *
40
+ * Este comando abria um browser e pedia aprovação mesmo com um token válido
41
+ * na máquina, para o mesmo host. A install mission manda logar depois de a
42
+ * pessoa já ter logado no navegador, e o wizard manda logar no terminal - de
43
+ * modo que "faça de novo o que você acabou de fazer" era o caminho normal, e
44
+ * não um caso de borda. Pedir de novo o que já foi feito é a coisa que faz
45
+ * alguém desconfiar de que o produto sabe o que está acontecendo.
46
+ *
47
+ * Sai por aqui só quando o token é para ESTE host: um token de outro
48
+ * registry não serve, e reconhecê-lo seria um 401 mais tarde disfarçado de
49
+ * sucesso agora.
50
+ */
51
+ if (!opts.force) {
52
+ const existing = await readCredentials();
53
+ if (existing && sameRegistry(existing.registry, base)) {
54
+ console.log("");
55
+ console.log(`✓ Already signed in to ${base} on this machine.`);
56
+ console.log(" Run `synthesisui login --force` to sign in as someone else.");
57
+ return;
58
+ }
59
+ }
37
60
  const codeRes = await fetch(`${base}/api/auth/device/code`, {
38
61
  method: "POST",
39
62
  headers: { "content-type": "application/json" },
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.me
36
36
  const HELP = `synthesisui - bring SynthesisUI design systems into your project
37
37
 
38
38
  Usage - deterministic, FREE:
39
- synthesisui login [options] connect the CLI to your account (device-flow)
39
+ synthesisui login [options] connect the CLI to your account (device-flow; --force to switch accounts)
40
40
  synthesisui init [options] write _synthesisui/config.json (target, dirs); --ds to bring one in
41
41
  synthesisui list [options] list the published design systems
42
42
  synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
@@ -387,7 +387,10 @@ async function main() {
387
387
  break;
388
388
  }
389
389
  case "login":
390
- await login({ registry });
390
+ // `--force` porque o comando agora sai cedo quando esta máquina já tem
391
+ // sessão para este host - trocar de conta continua possível, e passa a
392
+ // ser dito em vez de ser o comportamento padrão.
393
+ await login({ registry, force: args.includes("--force") });
391
394
  break;
392
395
  case "init": {
393
396
  const target = typeof flags.target === "string" ? flags.target : undefined;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * O PASTE QUE TIRA AS QUATRO EDIÇÕES DA MÃO DA PESSOA.
3
+ *
4
+ * `init --ds` termina imprimindo uma seção "One-time setup" com três passos
5
+ * numerados para o humano fazer à mão: os dois `@import`, o `data-ds` na raiz
6
+ * e a fiação das fontes. Foi exatamente aí que 27/07 mediu 0% de fiação -
7
+ * ninguém fazia, e o sistema que acabara de chegar não aparecia na tela.
8
+ *
9
+ * A pessoa que roda esse comando tem um agente aberto ao lado. Então o fecho
10
+ * deixa de ser uma lista de tarefas e passa a ser uma coisa para colar.
11
+ *
12
+ * GÊMEO POR SPEC com `apps/web/src/lib/ds/install-mission.ts`, que serve o
13
+ * mesmo texto na tela do onboarding - `wiring-prompt.spec.ts` reprova o drift.
14
+ * O CLI é publicado standalone e não pode importar de `apps/web`, então a
15
+ * cópia é inevitável; o que não é inevitável é ela divergir em silêncio, e um
16
+ * prompt que diverge entre a tela e o terminal ensina duas fiações diferentes
17
+ * para o mesmo sistema.
18
+ */
19
+ export function initCommand(slug) {
20
+ return `npx synthesisui@latest init --styles tailwind --ds ${slug}`;
21
+ }
22
+ export function wiringPrompt(slug) {
23
+ return `Wire the "${slug}" design system into this project. I already ran the install in my terminal.
24
+
25
+ 1. Run: npx synthesisui@latest doctor
26
+ If it says "no system installed", I skipped the install - run ${initCommand(slug)} first, then carry on.
27
+ 2. Do the ONE-TIME SETUP it names, all of it: the two @import lines in the project's global stylesheet (the path is relative to that file), data-ds="${slug}" on the root element, and the font wiring - importing from the fonts file it writes and mapping those variables in the stylesheet. The type is the step people skip.
28
+ 3. Run doctor again. It must NOT say "not wired up yet". If it does, it names exactly which piece is missing - fix that and run it again.
29
+ 4. Run: npx synthesisui@latest connect
30
+ If it answers "already had it" on both lines, everything is live and you are done.
31
+ If it WROTE either of them, then they are not live in this session, because hooks and tools are only read at startup. Stop there and tell me, in bold, on its own line: **RESTART THIS SESSION - the check is installed but not running yet.** Do not keep writing files after that; a session that cannot be checked is the state this whole setup exists to avoid.
32
+
33
+ Do not change any of my existing styles.`;
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.241",
3
+ "version": "0.16.243",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {