synthesisui 0.3.0 → 0.4.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.
@@ -0,0 +1,144 @@
1
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { basename, join } from "node:path";
3
+ import { generateComponentFiles } from "../component-codegen.js";
4
+ import { readProjectConfig, resolveRegistry } from "../config.js";
5
+ import { body, section, snippet } from "../output.js";
6
+ import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
7
+ /** Slugs materialized under `_synthesisui/ds/` in the project. */
8
+ async function installedSlugs(root) {
9
+ try {
10
+ const entries = await readdir(join(root, "_synthesisui", "ds"), {
11
+ withFileTypes: true,
12
+ });
13
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ /**
20
+ * Marco B5 - the REVERSE bridge, from the CLI: takes a component that lives in
21
+ * YOUR app (arbitrary React/CSS), re-expresses it in the design system's
22
+ * token vocabulary (hosted refit - gated + metered), SAVES it into your
23
+ * personal DS draft (it ships with the next `publish`), and materializes it
24
+ * back into componentsDir as your typed component. One command closes the
25
+ * loop: app code → on-system recipe → back as code.
26
+ */
27
+ export async function refit(file, opts) {
28
+ const base = resolveRegistry(opts.registry);
29
+ const root = opts.dir ?? process.cwd();
30
+ // 1. the source component (the server caps at 24k chars - fail fast here)
31
+ let source;
32
+ try {
33
+ source = await readFile(join(root, file), "utf8");
34
+ }
35
+ catch {
36
+ throw new RegistryError(`Could not read "${file}".`);
37
+ }
38
+ if (source.trim().length === 0) {
39
+ throw new RegistryError(`"${file}" is empty.`);
40
+ }
41
+ if (source.length > 24_000) {
42
+ throw new RegistryError(`"${file}" is ${source.length} chars - the refit cap is 24k. Trim it to the component itself.`);
43
+ }
44
+ let support;
45
+ if (opts.support) {
46
+ try {
47
+ support = await readFile(join(root, opts.support), "utf8");
48
+ }
49
+ catch {
50
+ throw new RegistryError(`Could not read support file "${opts.support}".`);
51
+ }
52
+ }
53
+ // 2. target DS (same inference as `generate`: the single installed one)
54
+ let slug = opts.ds;
55
+ if (!slug) {
56
+ const slugs = await installedSlugs(root);
57
+ if (slugs.length === 1)
58
+ slug = slugs[0];
59
+ else if (slugs.length === 0)
60
+ throw new RegistryError("No design system installed here. Run `synthesisui add <slug>` first, or pass --ds <slug>.");
61
+ else
62
+ throw new RegistryError(`Multiple design systems installed (${slugs.join(", ")}). Pick one with --ds <slug>.`);
63
+ }
64
+ // 3. replace mode: fetch the existing recipe so the server keeps its name
65
+ // (deterministic - the AI is not trusted with it)
66
+ let prior;
67
+ if (opts.replace) {
68
+ const existing = await fetchComponent(base, slug, opts.replace);
69
+ prior = { name: existing.name, recipe: existing.recipe };
70
+ }
71
+ console.log(`→ refitting ${basename(file)} into "${slug}"${prior ? ` (replacing ds-${prior.name})` : ""} …`);
72
+ const instruction = [
73
+ opts.instruction,
74
+ !prior && opts.name ? `Name it "${opts.name}".` : undefined,
75
+ ]
76
+ .filter(Boolean)
77
+ .join(" ");
78
+ const res = await postRefit(base, {
79
+ slug,
80
+ source,
81
+ support,
82
+ instruction: instruction || undefined,
83
+ prior,
84
+ });
85
+ const tries = `${res.tries} ${res.tries === 1 ? "try" : "tries"}`;
86
+ console.log(`✓ adapted as ds-${res.name} (${res.model}, ${tries})`);
87
+ if (opts.dry) {
88
+ console.log(section("Dry run - nothing saved"));
89
+ console.log(body("The recipe it would save:"));
90
+ console.log("");
91
+ console.log(snippet(JSON.stringify(res.recipe, null, 2).split("\n")));
92
+ console.log("");
93
+ return;
94
+ }
95
+ // 4. save into the personal DS draft (server re-validates token-only)
96
+ const saved = await postSaveComponent(base, {
97
+ slug,
98
+ name: res.name,
99
+ recipe: res.recipe,
100
+ });
101
+ console.log(`✓ saved into "${slug}" (draft v${saved.version} - ships with your next publish)`);
102
+ // 5. materialize back into the project: artifacts + YOUR typed component
103
+ const artifactsDir = join(root, "_synthesisui", "ds", slug, "components");
104
+ await mkdir(artifactsDir, { recursive: true });
105
+ await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
106
+ await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
107
+ const config = await readProjectConfig(root);
108
+ let materialized = false;
109
+ if (config.target === "next") {
110
+ const compDir = join(root, config.componentsDir, res.name);
111
+ await mkdir(compDir, { recursive: true });
112
+ const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles);
113
+ for (const f of files) {
114
+ await writeFile(join(compDir, f.filename), f.code, "utf8");
115
+ }
116
+ materialized = true;
117
+ console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
118
+ }
119
+ if (res.suggestedRule) {
120
+ console.log(section("Suggested rule"));
121
+ console.log(body("The AI inferred a reusable rule from this component:"));
122
+ console.log("");
123
+ console.log(snippet([`"${res.suggestedRule}"`]));
124
+ console.log("");
125
+ console.log(body(`(save it in the studio if it holds: /dashboard/mine/${slug}/studio)`));
126
+ }
127
+ console.log(section("Done - the loop is closed"));
128
+ console.log(body(`Your component now lives in the design system (docs, studio, showcase)`));
129
+ if (materialized) {
130
+ const pascal = res.name
131
+ .split(/[^a-zA-Z0-9]+/)
132
+ .filter(Boolean)
133
+ .map((p) => p[0].toUpperCase() + p.slice(1))
134
+ .join("");
135
+ console.log(body(`and back in your code, on-system:`));
136
+ console.log("");
137
+ console.log(snippet([
138
+ `import { ${pascal} } from "@/${config.componentsDir}/${res.name}";`,
139
+ ]));
140
+ console.log("");
141
+ console.log(body(`(replace the old ${basename(file)} usages with it when you're ready)`));
142
+ }
143
+ console.log("");
144
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { generate } from "./commands/generate.js";
6
6
  import { init } from "./commands/init.js";
7
7
  import { list } from "./commands/list.js";
8
8
  import { login } from "./commands/login.js";
9
+ import { refit } from "./commands/refit.js";
9
10
  import { template } from "./commands/template.js";
10
11
  import { upgrade } from "./commands/upgrade.js";
11
12
  import { use } from "./commands/use.js";
@@ -20,6 +21,7 @@ Usage:
20
21
  synthesisui template <slug> <name> materialize a whole page from a DS template
21
22
  synthesisui component <slug> <name> bring one component in - artifacts + YOUR <Pascal>.tsx in componentsDir
22
23
  synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
24
+ synthesisui refit <file> [--ds <slug>] send an app component INTO your DS (token-only) and get it back as code
23
25
  synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
24
26
  synthesisui advise "<value prop>" engagement-pattern proposals for this project (login required)
25
27
  synthesisui generate "<desc>" generate a token-only component recipe for your DS (login required)
@@ -35,6 +37,10 @@ Options:
35
37
  --components-dir <dir> init: folder where components live (default: components)
36
38
  --styles <s> init: component code flavor: css | tailwind (default: css)
37
39
  --artifacts-only component: skip the .tsx materialization (recipe + css only)
40
+ --replace <name> refit: replace an existing DS component (keeps its name)
41
+ --support <file> refit: supporting CSS file (globals/vars the code references)
42
+ --instruction <s> refit: extra guidance for the adaptation
43
+ --dry refit: adapt and print, but save nothing
38
44
  --out <path> output path for the generated template (default: <pagesDir>/<file>)
39
45
  -h, --help this help
40
46
 
@@ -194,6 +200,25 @@ async function main() {
194
200
  });
195
201
  break;
196
202
  }
203
+ case "refit": {
204
+ const file = args[0];
205
+ if (!file) {
206
+ console.error("error: provide the component file - `synthesisui refit <file> [--ds <slug>]`");
207
+ process.exitCode = 1;
208
+ return;
209
+ }
210
+ await refit(file, {
211
+ registry,
212
+ dir,
213
+ ds: typeof flags.ds === "string" ? flags.ds : undefined,
214
+ name: typeof flags.name === "string" ? flags.name : undefined,
215
+ replace: typeof flags.replace === "string" ? flags.replace : undefined,
216
+ instruction: typeof flags.instruction === "string" ? flags.instruction : undefined,
217
+ support: typeof flags.support === "string" ? flags.support : undefined,
218
+ dry: flags.dry === true,
219
+ });
220
+ break;
221
+ }
197
222
  case "upgrade": {
198
223
  const slug = args[0];
199
224
  if (!slug) {
package/dist/registry.js CHANGED
@@ -150,3 +150,57 @@ export async function fetchChangelog(base, slug, from, to) {
150
150
  }
151
151
  return (await res.json());
152
152
  }
153
+ /**
154
+ * Refit hospedado (INS-18 / Marco B5): manda código de componente arbitrário e
155
+ * recebe a recipe token-only vestida no DS. Gated + metered server-side:
156
+ * 401 = sem login, 429 = cota diária.
157
+ */
158
+ export async function postRefit(base, payload) {
159
+ let res;
160
+ try {
161
+ res = await fetch(`${base}/api/ai/studio`, {
162
+ method: "POST",
163
+ headers: { "content-type": "application/json", ...(await authHeaders()) },
164
+ body: JSON.stringify(payload),
165
+ });
166
+ }
167
+ catch {
168
+ throw new RegistryError(`Could not reach the registry at ${base}. ` +
169
+ `Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
170
+ }
171
+ if (res.status === 401) {
172
+ throw new RegistryError("Not authenticated. Run `synthesisui login` first.");
173
+ }
174
+ if (!res.ok) {
175
+ const body = (await res.json().catch(() => ({})));
176
+ throw new RegistryError(body.message ?? `Refit responded ${res.status}.`);
177
+ }
178
+ return (await res.json());
179
+ }
180
+ /**
181
+ * Persiste um componente no DS pessoal do usuário autenticado
182
+ * (`POST /api/ds/component`) - a metade "salvar" da ponte reversa. O servidor
183
+ * re-valida a recipe (token-only, sem refs órfãs) antes de gravar no rascunho.
184
+ */
185
+ export async function postSaveComponent(base, payload) {
186
+ let res;
187
+ try {
188
+ res = await fetch(`${base}/api/ds/component`, {
189
+ method: "POST",
190
+ headers: { "content-type": "application/json", ...(await authHeaders()) },
191
+ body: JSON.stringify(payload),
192
+ });
193
+ }
194
+ catch {
195
+ throw new RegistryError(`Could not reach the registry at ${base}. ` +
196
+ `Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
197
+ }
198
+ if (res.status === 401) {
199
+ throw new RegistryError("Not authenticated. Run `synthesisui login` first.");
200
+ }
201
+ if (!res.ok) {
202
+ const body = (await res.json().catch(() => ({})));
203
+ throw new RegistryError(body.message ?? `Save responded ${res.status}.`);
204
+ }
205
+ return (await res.json());
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
5
  "type": "module",
6
6
  "bin": {